Add battery to energy dashboard (#9757)

pull/9787/head
Bram Kragten 2021-08-12 17:40:21 +02:00 committed by GitHub
parent 44548fdc33
commit 19e4c0657a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 954 additions and 98 deletions

View File

@ -44,6 +44,11 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
stat_energy_from: "sensor.solar_production", stat_energy_from: "sensor.solar_production",
config_entry_solar_forecast: ["solar_forecast"], config_entry_solar_forecast: ["solar_forecast"],
}, },
{
type: "battery",
stat_energy_from: "sensor.battery_output",
stat_energy_to: "sensor.battery_input",
},
], ],
device_consumption: [ device_consumption: [
{ {

View File

@ -18,6 +18,24 @@ export const energyEntities = () =>
unit_of_measurement: "kWh", unit_of_measurement: "kWh",
}, },
}, },
"sensor.battery_input": {
entity_id: "sensor.battery_input",
state: "4",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Battery Input",
unit_of_measurement: "kWh",
},
},
"sensor.battery_output": {
entity_id: "sensor.battery_output",
state: "3",
attributes: {
last_reset: "1970-01-01T00:00:00:00+00",
friendly_name: "Battery Output",
unit_of_measurement: "kWh",
},
},
"sensor.energy_consumption_tarif_1": { "sensor.energy_consumption_tarif_1": {
entity_id: "sensor.energy_consumption_tarif_1 ", entity_id: "sensor.energy_consumption_tarif_1 ",
state: "88.6", state: "88.6",

View File

@ -47,6 +47,13 @@ export const emptySolarEnergyPreference =
config_entry_solar_forecast: null, config_entry_solar_forecast: null,
}); });
export const emptyBatteryEnergyPreference =
(): BatterySourceTypeEnergyPreference => ({
type: "battery",
stat_energy_from: "",
stat_energy_to: "",
});
export interface DeviceConsumptionEnergyPreference { export interface DeviceConsumptionEnergyPreference {
// This is an ever increasing value // This is an ever increasing value
stat_consumption: string; stat_consumption: string;
@ -94,9 +101,16 @@ export interface SolarSourceTypeEnergyPreference {
config_entry_solar_forecast: string[] | null; config_entry_solar_forecast: string[] | null;
} }
export interface BatterySourceTypeEnergyPreference {
type: "battery";
stat_energy_from: string;
stat_energy_to: string;
}
type EnergySource = type EnergySource =
| SolarSourceTypeEnergyPreference | SolarSourceTypeEnergyPreference
| GridSourceTypeEnergyPreference; | GridSourceTypeEnergyPreference
| BatterySourceTypeEnergyPreference;
export interface EnergyPreferences { export interface EnergyPreferences {
energy_sources: EnergySource[]; energy_sources: EnergySource[];
@ -132,6 +146,7 @@ export const saveEnergyPreferences = async (
interface EnergySourceByType { interface EnergySourceByType {
grid?: GridSourceTypeEnergyPreference[]; grid?: GridSourceTypeEnergyPreference[];
solar?: SolarSourceTypeEnergyPreference[]; solar?: SolarSourceTypeEnergyPreference[];
battery?: BatterySourceTypeEnergyPreference[];
} }
export const energySourcesByType = (prefs: EnergyPreferences) => { export const energySourcesByType = (prefs: EnergyPreferences) => {
@ -203,6 +218,12 @@ const getEnergyData = async (
continue; continue;
} }
if (source.type === "battery") {
statIDs.push(source.stat_energy_from);
statIDs.push(source.stat_energy_to);
continue;
}
// grid source // grid source
for (const flowFrom of source.flow_from) { for (const flowFrom of source.flow_from) {
statIDs.push(flowFrom.stat_energy_from); statIDs.push(flowFrom.stat_energy_from);

View File

@ -0,0 +1,177 @@
import "@material/mwc-button/mwc-button";
import { mdiBatteryHigh, mdiDelete, mdiPencil } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-card";
import "../../../../components/ha-settings-row";
import {
BatterySourceTypeEnergyPreference,
EnergyPreferences,
energySourcesByType,
saveEnergyPreferences,
} from "../../../../data/energy";
import {
showConfirmationDialog,
showAlertDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { showEnergySettingsBatteryDialog } from "../dialogs/show-dialogs-energy";
import { energyCardStyles } from "./styles";
@customElement("ha-energy-battery-settings")
export class EnergyBatterySettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public preferences!: EnergyPreferences;
protected render(): TemplateResult {
const types = energySourcesByType(this.preferences);
const batterySources = types.battery || [];
return html`
<ha-card>
<h1 class="card-header">
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
${this.hass.localize("ui.panel.config.energy.battery.title")}
</h1>
<div class="card-content">
<p>
${this.hass.localize("ui.panel.config.energy.battery.sub")}
<a
target="_blank"
rel="noopener noreferrer"
href="${documentationUrl(this.hass, "/docs/energy/battery/")}"
>${this.hass.localize(
"ui.panel.config.energy.battery.learn_more"
)}</a
>
</p>
<h3>Battery systems</h3>
${batterySources.map((source) => {
const fromEntityState = this.hass.states[source.stat_energy_from];
const toEntityState = this.hass.states[source.stat_energy_to];
return html`
<div class="row" .source=${source}>
${toEntityState?.attributes.icon
? html`<ha-icon
.icon=${toEntityState.attributes.icon}
></ha-icon>`
: html`<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>`}
<div class="content">
<span
>${toEntityState
? computeStateName(toEntityState)
: source.stat_energy_from}</span
>
<span
>${fromEntityState
? computeStateName(fromEntityState)
: source.stat_energy_to}</span
>
</div>
<mwc-icon-button @click=${this._editSource}>
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>
<mwc-icon-button @click=${this._deleteSource}>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
</mwc-icon-button>
</div>
`;
})}
<div class="row border-bottom">
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
<mwc-button @click=${this._addSource}
>Add battery system</mwc-button
>
</div>
</div>
</ha-card>
`;
}
private _addSource() {
showEnergySettingsBatteryDialog(this, {
saveCallback: async (source) => {
await this._savePreferences({
...this.preferences,
energy_sources: this.preferences.energy_sources.concat(source),
});
},
});
}
private _editSource(ev) {
const origSource: BatterySourceTypeEnergyPreference =
ev.currentTarget.closest(".row").source;
showEnergySettingsBatteryDialog(this, {
source: { ...origSource },
saveCallback: async (newSource) => {
await this._savePreferences({
...this.preferences,
energy_sources: this.preferences.energy_sources.map((src) =>
src === origSource ? newSource : src
),
});
},
});
}
private async _deleteSource(ev) {
const sourceToDelete: BatterySourceTypeEnergyPreference =
ev.currentTarget.closest(".row").source;
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want to delete this source?",
}))
) {
return;
}
try {
await this._savePreferences({
...this.preferences,
energy_sources: this.preferences.energy_sources.filter(
(source) => source !== sourceToDelete
),
});
} catch (err) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
}
}
private async _savePreferences(preferences: EnergyPreferences) {
const result = await saveEnergyPreferences(this.hass, preferences);
fireEvent(this, "value-changed", { value: result });
}
static get styles(): CSSResultGroup {
return [
haStyle,
energyCardStyles,
css`
.row {
height: 58px;
}
.content {
display: flex;
flex-direction: column;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-energy-battery-settings": EnergyBatterySettings;
}
}

View File

@ -0,0 +1,132 @@
import { mdiBatteryHigh } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-dialog";
import {
BatterySourceTypeEnergyPreference,
emptyBatteryEnergyPreference,
} from "../../../../data/energy";
import { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
import "@material/mwc-button/mwc-button";
import "../../../../components/entity/ha-statistic-picker";
const energyUnits = ["kWh"];
@customElement("dialog-energy-battery-settings")
export class DialogEnergyBatterySettings
extends LitElement
implements HassDialog<EnergySettingsBatteryDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EnergySettingsBatteryDialogParams;
@state() private _source?: BatterySourceTypeEnergyPreference;
@state() private _error?: string;
public async showDialog(
params: EnergySettingsBatteryDialogParams
): Promise<void> {
this._params = params;
this._source = params.source
? { ...params.source }
: (this._source = emptyBatteryEnergyPreference());
}
public closeDialog(): void {
this._params = undefined;
this._source = undefined;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params || !this._source) {
return html``;
}
return html`
<ha-dialog
open
.heading=${html`<ha-svg-icon
.path=${mdiBatteryHigh}
style="--mdc-icon-size: 32px;"
></ha-svg-icon>
Configure battery system`}
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<ha-statistic-picker
.hass=${this.hass}
.includeUnitOfMeasurement=${energyUnits}
.value=${this._source.stat_energy_to}
.label=${`Energy going in to the battery (kWh)`}
entities-only
@value-changed=${this._statisticToChanged}
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.includeUnitOfMeasurement=${energyUnits}
.value=${this._source.stat_energy_from}
.label=${`Energy coming out of the battery (kWh)`}
entities-only
@value-changed=${this._statisticFromChanged}
></ha-statistic-picker>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
@click=${this._save}
.disabled=${!this._source.stat_energy_from ||
!this._source.stat_energy_to}
slot="primaryAction"
>
${this.hass.localize("ui.common.save")}
</mwc-button>
</ha-dialog>
`;
}
private _statisticToChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_energy_to: ev.detail.value };
}
private _statisticFromChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
private async _save() {
try {
await this._params!.saveCallback(this._source!);
this.closeDialog();
} catch (e) {
this._error = e.message;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 430px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-energy-battery-settings": DialogEnergyBatterySettings;
}
}

View File

@ -17,7 +17,6 @@ import "../../../../components/ha-radio";
import "../../../../components/ha-checkbox"; import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox"; import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield"; import "../../../../components/ha-formfield";
import "../../../../components/entity/ha-entity-picker";
import type { HaRadio } from "../../../../components/ha-radio"; import type { HaRadio } from "../../../../components/ha-radio";
import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow"; import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow";
import { ConfigEntry, getConfigEntries } from "../../../../data/config_entries"; import { ConfigEntry, getConfigEntries } from "../../../../data/config_entries";

View File

@ -1,5 +1,6 @@
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { import {
BatterySourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference, DeviceConsumptionEnergyPreference,
FlowFromGridSourceEnergyPreference, FlowFromGridSourceEnergyPreference,
FlowToGridSourceEnergyPreference, FlowToGridSourceEnergyPreference,
@ -33,6 +34,11 @@ export interface EnergySettingsSolarDialogParams {
saveCallback: (source: SolarSourceTypeEnergyPreference) => Promise<void>; saveCallback: (source: SolarSourceTypeEnergyPreference) => Promise<void>;
} }
export interface EnergySettingsBatteryDialogParams {
source?: BatterySourceTypeEnergyPreference;
saveCallback: (source: BatterySourceTypeEnergyPreference) => Promise<void>;
}
export interface EnergySettingsDeviceDialogParams { export interface EnergySettingsDeviceDialogParams {
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>; saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
} }
@ -48,6 +54,17 @@ export const showEnergySettingsDeviceDialog = (
}); });
}; };
export const showEnergySettingsBatteryDialog = (
element: HTMLElement,
dialogParams: EnergySettingsBatteryDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-energy-battery-settings",
dialogImport: () => import("./dialog-energy-battery-settings"),
dialogParams: dialogParams,
});
};
export const showEnergySettingsSolarDialog = ( export const showEnergySettingsSolarDialog = (
element: HTMLElement, element: HTMLElement,
dialogParams: EnergySettingsSolarDialogParams dialogParams: EnergySettingsSolarDialogParams

View File

@ -10,6 +10,7 @@ import { configSections } from "../ha-panel-config";
import "./components/ha-energy-device-settings"; import "./components/ha-energy-device-settings";
import "./components/ha-energy-grid-settings"; import "./components/ha-energy-grid-settings";
import "./components/ha-energy-solar-settings"; import "./components/ha-energy-solar-settings";
import "./components/ha-energy-battery-settings";
const INITIAL_CONFIG: EnergyPreferences = { const INITIAL_CONFIG: EnergyPreferences = {
energy_sources: [], energy_sources: [],
@ -81,6 +82,11 @@ class HaConfigEnergy extends LitElement {
.preferences=${this._preferences!} .preferences=${this._preferences!}
@value-changed=${this._prefsChanged} @value-changed=${this._prefsChanged}
></ha-energy-solar-settings> ></ha-energy-solar-settings>
<ha-energy-battery-settings
.hass=${this.hass}
.preferences=${this._preferences!}
@value-changed=${this._prefsChanged}
></ha-energy-battery-settings>
<ha-energy-device-settings <ha-energy-device-settings
.hass=${this.hass} .hass=${this.hass}
.preferences=${this._preferences!} .preferences=${this._preferences!}

View File

@ -8,6 +8,7 @@ import { LovelaceCard, Lovelace } from "../../lovelace/types";
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "../../config/energy/components/ha-energy-grid-settings"; import "../../config/energy/components/ha-energy-grid-settings";
import "../../config/energy/components/ha-energy-solar-settings"; import "../../config/energy/components/ha-energy-solar-settings";
import "../../config/energy/components/ha-energy-battery-settings";
import "../../config/energy/components/ha-energy-device-settings"; import "../../config/energy/components/ha-energy-device-settings";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
@ -43,18 +44,24 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
return html` return html`
<p>Step ${this._step + 1} of 3</p> <p>Step ${this._step + 1} of 3</p>
${this._step === 0 ${this._step === 0
? html` <ha-energy-grid-settings ? html`<ha-energy-grid-settings
.hass=${this.hass} .hass=${this.hass}
.preferences=${this._preferences} .preferences=${this._preferences}
@value-changed=${this._prefsChanged} @value-changed=${this._prefsChanged}
></ha-energy-grid-settings>` ></ha-energy-grid-settings>`
: this._step === 1 : this._step === 1
? html` <ha-energy-solar-settings ? html`<ha-energy-solar-settings
.hass=${this.hass} .hass=${this.hass}
.preferences=${this._preferences} .preferences=${this._preferences}
@value-changed=${this._prefsChanged} @value-changed=${this._prefsChanged}
></ha-energy-solar-settings>` ></ha-energy-solar-settings>`
: html` <ha-energy-device-settings : this._step === 2
? html`<ha-energy-battery-settings
.hass=${this.hass}
.preferences=${this._preferences}
@value-changed=${this._prefsChanged}
></ha-energy-battery-settings>`
: html`<ha-energy-device-settings
.hass=${this.hass} .hass=${this.hass}
.preferences=${this._preferences} .preferences=${this._preferences}
@value-changed=${this._prefsChanged} @value-changed=${this._prefsChanged}
@ -65,7 +72,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
>${this.hass.localize("ui.panel.energy.setup.back")}</mwc-button >${this.hass.localize("ui.panel.energy.setup.back")}</mwc-button
>` >`
: html`<div></div>`} : html`<div></div>`}
${this._step < 2 ${this._step < 3
? html`<mwc-button unelevated @click=${this._next} ? html`<mwc-button unelevated @click=${this._next}
>${this.hass.localize("ui.panel.energy.setup.next")}</mwc-button >${this.hass.localize("ui.panel.energy.setup.next")}</mwc-button
>` >`

View File

@ -1,6 +1,9 @@
import { import {
mdiArrowDown,
mdiArrowLeft, mdiArrowLeft,
mdiArrowRight, mdiArrowRight,
mdiArrowUp,
mdiBatteryHigh,
mdiHome, mdiHome,
mdiLeaf, mdiLeaf,
mdiSolarPower, mdiSolarPower,
@ -75,9 +78,10 @@ class HuiEnergyDistrubutionCard
const hasConsumption = true; const hasConsumption = true;
const hasSolarProduction = types.solar !== undefined; const hasSolarProduction = types.solar !== undefined;
const hasBattery = types.battery !== undefined;
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
const totalGridConsumption = const totalFromGrid =
calculateStatisticsSumGrowth( calculateStatisticsSumGrowth(
this._data.stats, this._data.stats,
types.grid![0].flow_from.map((flow) => flow.stat_energy_from) types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
@ -93,30 +97,97 @@ class HuiEnergyDistrubutionCard
) || 0; ) || 0;
} }
let productionReturnedToGrid: number | null = null; let totalBatteryIn: number | null = null;
let totalBatteryOut: number | null = null;
if (hasBattery) {
totalBatteryIn =
calculateStatisticsSumGrowth(
this._data.stats,
types.battery!.map((source) => source.stat_energy_to)
) || 0;
totalBatteryOut =
calculateStatisticsSumGrowth(
this._data.stats,
types.battery!.map((source) => source.stat_energy_from)
) || 0;
}
let returnedToGrid: number | null = null;
if (hasReturnToGrid) { if (hasReturnToGrid) {
productionReturnedToGrid = returnedToGrid =
calculateStatisticsSumGrowth( calculateStatisticsSumGrowth(
this._data.stats, this._data.stats,
types.grid![0].flow_to.map((flow) => flow.stat_energy_to) types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
) || 0; ) || 0;
} }
const solarConsumption = Math.max( let solarConsumption: number | null = null;
0, if (hasSolarProduction) {
(totalSolarProduction || 0) - (productionReturnedToGrid || 0) solarConsumption =
); (totalSolarProduction || 0) -
(returnedToGrid || 0) -
(totalBatteryIn || 0);
}
const totalHomeConsumption = totalGridConsumption + solarConsumption; let batteryFromGrid: null | number = null;
let batteryToGrid: null | number = null;
if (solarConsumption !== null && solarConsumption < 0) {
// What we returned to the grid and what went in to the battery is more than produced,
// so we have used grid energy to fill the battery
// or returned battery energy to the grid
if (hasBattery) {
batteryFromGrid = solarConsumption * -1;
if (batteryFromGrid > totalFromGrid) {
batteryToGrid = Math.min(0, batteryFromGrid - totalFromGrid);
batteryFromGrid = totalFromGrid;
}
}
solarConsumption = 0;
}
let solarToBattery: null | number = null;
if (hasSolarProduction && hasBattery) {
if (!batteryToGrid) {
batteryToGrid = Math.max(
0,
(returnedToGrid || 0) -
(totalSolarProduction || 0) -
(totalBatteryIn || 0) -
(batteryFromGrid || 0)
);
}
solarToBattery = totalBatteryIn! - (batteryFromGrid || 0);
} else if (!hasSolarProduction && hasBattery) {
batteryToGrid = returnedToGrid;
}
let batteryConsumption: number | null = null;
if (hasBattery) {
batteryConsumption = (totalBatteryOut || 0) - (batteryToGrid || 0);
}
const gridConsumption = Math.max(0, totalFromGrid - (batteryFromGrid || 0));
const totalHomeConsumption = Math.max(
0,
gridConsumption + (solarConsumption || 0) + (batteryConsumption || 0)
);
let homeSolarCircumference: number | undefined; let homeSolarCircumference: number | undefined;
if (hasSolarProduction) { if (hasSolarProduction) {
homeSolarCircumference = homeSolarCircumference =
CIRCLE_CIRCUMFERENCE * (solarConsumption / totalHomeConsumption); CIRCLE_CIRCUMFERENCE * (solarConsumption! / totalHomeConsumption);
} }
let lowCarbonConsumption: number | undefined; let homeBatteryCircumference: number | undefined;
if (batteryConsumption) {
homeBatteryCircumference =
CIRCLE_CIRCUMFERENCE * (batteryConsumption / totalHomeConsumption);
}
let lowCarbonEnergy: number | undefined;
let homeLowCarbonCircumference: number | undefined; let homeLowCarbonCircumference: number | undefined;
let homeHighCarbonCircumference: number | undefined; let homeHighCarbonCircumference: number | undefined;
@ -129,7 +200,7 @@ class HuiEnergyDistrubutionCard
this._data.co2SignalEntity in this._data.stats this._data.co2SignalEntity in this._data.stats
) { ) {
// Calculate high carbon consumption // Calculate high carbon consumption
const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage( const highCarbonEnergy = calculateStatisticsSumGrowthWithPercentage(
this._data.stats[this._data.co2SignalEntity], this._data.stats[this._data.co2SignalEntity],
types types
.grid![0].flow_from.map( .grid![0].flow_from.map(
@ -144,8 +215,17 @@ class HuiEnergyDistrubutionCard
electricityMapUrl += `/zone/${co2State.attributes.country_code}`; electricityMapUrl += `/zone/${co2State.attributes.country_code}`;
} }
if (highCarbonConsumption !== null) { if (highCarbonEnergy !== null) {
lowCarbonConsumption = totalGridConsumption - highCarbonConsumption; lowCarbonEnergy = totalFromGrid - highCarbonEnergy;
let highCarbonConsumption: number;
if (gridConsumption !== totalFromGrid) {
// Only get the part that was used for consumption and not the battery
highCarbonConsumption =
highCarbonEnergy * (gridConsumption / totalFromGrid);
} else {
highCarbonConsumption = highCarbonEnergy;
}
homeHighCarbonCircumference = homeHighCarbonCircumference =
CIRCLE_CIRCUMFERENCE * (highCarbonConsumption / totalHomeConsumption); CIRCLE_CIRCUMFERENCE * (highCarbonConsumption / totalHomeConsumption);
@ -153,16 +233,26 @@ class HuiEnergyDistrubutionCard
homeLowCarbonCircumference = homeLowCarbonCircumference =
CIRCLE_CIRCUMFERENCE - CIRCLE_CIRCUMFERENCE -
(homeSolarCircumference || 0) - (homeSolarCircumference || 0) -
(homeBatteryCircumference || 0) -
homeHighCarbonCircumference; homeHighCarbonCircumference;
} }
} }
const totalLines =
gridConsumption +
(solarConsumption || 0) +
(returnedToGrid ? returnedToGrid - (batteryToGrid || 0) : 0) +
(solarToBattery || 0) +
(batteryConsumption || 0) +
(batteryFromGrid || 0) +
(batteryToGrid || 0);
return html` return html`
<ha-card .header=${this._config.title}> <ha-card .header=${this._config.title}>
<div class="card-content"> <div class="card-content">
${lowCarbonConsumption !== undefined || hasSolarProduction ${lowCarbonEnergy !== undefined || hasSolarProduction
? html`<div class="row"> ? html`<div class="row">
${lowCarbonConsumption === undefined ${lowCarbonEnergy === undefined
? html`<div class="spacer"></div>` ? html`<div class="spacer"></div>`
: html`<div class="circle-container low-carbon"> : html`<div class="circle-container low-carbon">
<span class="label">Non-fossil</span> <span class="label">Non-fossil</span>
@ -173,12 +263,10 @@ class HuiEnergyDistrubutionCard
rel="noopener no referrer" rel="noopener no referrer"
> >
<ha-svg-icon .path="${mdiLeaf}"></ha-svg-icon> <ha-svg-icon .path="${mdiLeaf}"></ha-svg-icon>
${lowCarbonConsumption ${lowCarbonEnergy
? formatNumber( ? formatNumber(lowCarbonEnergy, this.hass.locale, {
lowCarbonConsumption, maximumFractionDigits: 1,
this.hass.locale, })
{ maximumFractionDigits: 1 }
)
: "-"} : "-"}
kWh kWh
</a> </a>
@ -207,33 +295,29 @@ class HuiEnergyDistrubutionCard
<div class="circle-container grid"> <div class="circle-container grid">
<div class="circle"> <div class="circle">
<ha-svg-icon .path="${mdiTransmissionTower}"></ha-svg-icon> <ha-svg-icon .path="${mdiTransmissionTower}"></ha-svg-icon>
${returnedToGrid !== null
? html`<span class="return">
<ha-svg-icon
class="small"
.path=${mdiArrowLeft}
></ha-svg-icon
>${formatNumber(returnedToGrid, this.hass.locale, {
maximumFractionDigits: 1,
})}
kWh
</span>`
: ""}
<span class="consumption"> <span class="consumption">
${hasReturnToGrid ${hasReturnToGrid
? html`<ha-svg-icon ? html`<ha-svg-icon
class="small" class="small"
.path=${mdiArrowRight} .path=${mdiArrowRight}
></ha-svg-icon>` ></ha-svg-icon>`
: ""}${formatNumber( : ""}${formatNumber(totalFromGrid, this.hass.locale, {
totalGridConsumption, maximumFractionDigits: 1,
this.hass.locale, })}
{ maximumFractionDigits: 1 }
)}
kWh kWh
</span> </span>
${productionReturnedToGrid !== null
? html`<span class="return">
<ha-svg-icon
class="small"
.path=${mdiArrowLeft}
></ha-svg-icon
>${formatNumber(
productionReturnedToGrid,
this.hass.locale,
{ maximumFractionDigits: 1 }
)}
kWh
</span>`
: ""}
</div> </div>
<span class="label">Grid</span> <span class="label">Grid</span>
</div> </div>
@ -268,6 +352,23 @@ class HuiEnergyDistrubutionCard
}" }"
/>` />`
: ""} : ""}
${homeBatteryCircumference
? svg`<circle
class="battery"
cx="40"
cy="40"
r="38"
stroke-dasharray="${homeBatteryCircumference} ${
CIRCLE_CIRCUMFERENCE - homeBatteryCircumference
}"
stroke-dashoffset="-${
CIRCLE_CIRCUMFERENCE -
homeBatteryCircumference -
(homeSolarCircumference || 0)
}"
shape-rendering="geometricPrecision"
/>`
: ""}
${homeLowCarbonCircumference ${homeLowCarbonCircumference
? svg`<circle ? svg`<circle
class="low-carbon" class="low-carbon"
@ -280,6 +381,7 @@ class HuiEnergyDistrubutionCard
stroke-dashoffset="-${ stroke-dashoffset="-${
CIRCLE_CIRCUMFERENCE - CIRCLE_CIRCUMFERENCE -
homeLowCarbonCircumference - homeLowCarbonCircumference -
(homeBatteryCircumference || 0) -
(homeSolarCircumference || 0) (homeSolarCircumference || 0)
}" }"
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
@ -292,10 +394,12 @@ class HuiEnergyDistrubutionCard
r="38" r="38"
stroke-dasharray="${homeHighCarbonCircumference ?? stroke-dasharray="${homeHighCarbonCircumference ??
CIRCLE_CIRCUMFERENCE - CIRCLE_CIRCUMFERENCE -
homeSolarCircumference!} ${homeHighCarbonCircumference !== homeSolarCircumference! -
undefined (homeBatteryCircumference ||
0)} ${homeHighCarbonCircumference !== undefined
? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference ? CIRCLE_CIRCUMFERENCE - homeHighCarbonCircumference
: homeSolarCircumference}" : homeSolarCircumference! +
(homeBatteryCircumference || 0)}"
stroke-dashoffset="0" stroke-dashoffset="0"
shape-rendering="geometricPrecision" shape-rendering="geometricPrecision"
/> />
@ -305,7 +409,39 @@ class HuiEnergyDistrubutionCard
<span class="label">Home</span> <span class="label">Home</span>
</div> </div>
</div> </div>
<div class="lines"> ${hasBattery
? html`<div class="row">
<div class="spacer"></div>
<div class="circle-container battery">
<div class="circle">
<ha-svg-icon .path="${mdiBatteryHigh}"></ha-svg-icon>
<span class="battery-in">
<ha-svg-icon
class="small"
.path=${mdiArrowDown}
></ha-svg-icon
>${formatNumber(totalBatteryIn || 0, this.hass.locale, {
maximumFractionDigits: 1,
})}
kWh</span
>
<span class="battery-out">
<ha-svg-icon
class="small"
.path=${mdiArrowUp}
></ha-svg-icon>
${formatNumber(totalBatteryOut || 0, this.hass.locale, {
maximumFractionDigits: 1,
})}
kWh</span
>
</div>
<span class="label">Battery</span>
</div>
<div class="spacer"></div>
</div>`
: ""}
<div class="lines ${classMap({ battery: hasBattery })}">
<svg <svg
viewBox="0 0 100 100" viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -315,7 +451,9 @@ class HuiEnergyDistrubutionCard
? svg`<path ? svg`<path
id="return" id="return"
class="return" class="return"
d="M47,0 v15 c0,40 -10,35 -30,35 h-20" d="M${hasBattery ? 45 : 47},0 v15 c0,${
hasBattery ? "35 -10,30 -30,30" : "40 -10,35 -30,35"
} h-20"
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
></path> ` ></path> `
: ""} : ""}
@ -323,19 +461,45 @@ class HuiEnergyDistrubutionCard
? svg`<path ? svg`<path
id="solar" id="solar"
class="solar" class="solar"
d="M${ d="M${hasBattery ? 55 : 53},0 v15 c0,${
hasReturnToGrid ? 53 : 50 hasBattery ? "35 10,30 30,30" : "40 10,35 30,35"
},0 v15 c0,40 10,35 30,35 h20" } h20"
vector-effect="non-scaling-stroke"
></path>`
: ""}
${hasBattery
? svg`<path
id="battery-house"
class="battery-house"
d="M55,100 v-15 c0,-35 10,-30 30,-30 h20"
vector-effect="non-scaling-stroke"
></path>
<path
id="battery-grid"
class=${classMap({
"battery-from-grid": Boolean(batteryFromGrid),
"battery-to-grid": Boolean(batteryToGrid),
})}
d="M45,100 v-15 c0,-35 -10,-30 -30,-30 h-20"
vector-effect="non-scaling-stroke"
></path>
`
: ""}
${hasBattery && hasSolarProduction
? svg`<path
id="battery-solar"
class="battery-solar"
d="M50,0 V100"
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
></path>` ></path>`
: ""} : ""}
<path <path
class="grid" class="grid"
id="grid" id="grid"
d="M0,${hasSolarProduction ? 56 : 53} H100" d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100"
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
></path> ></path>
${productionReturnedToGrid && hasSolarProduction ${returnedToGrid && hasSolarProduction
? svg`<circle ? svg`<circle
r="1" r="1"
class="return" class="return"
@ -344,61 +508,107 @@ class HuiEnergyDistrubutionCard
<animateMotion <animateMotion
dur="${ dur="${
6 - 6 -
(productionReturnedToGrid / ((returnedToGrid - (batteryToGrid || 0)) / totalLines) *
(totalGridConsumption + 6
(totalSolarProduction || 0))) *
5
}s" }s"
repeatCount="indefinite" repeatCount="indefinite"
rotate="auto" calcMode="linear"
> >
<mpath xlink:href="#return" /> <mpath xlink:href="#return" />
</animateMotion> </animateMotion>
</circle>` </circle>`
: ""} : ""}
${totalSolarProduction ${solarConsumption
? svg`<circle ? svg`<circle
r="1" r="1"
class="solar" class="solar"
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
> >
<animateMotion <animateMotion
dur="${ dur="${6 - (solarConsumption / totalLines) * 5}s"
6 -
((totalSolarProduction -
(productionReturnedToGrid || 0)) /
(totalGridConsumption +
(totalSolarProduction || 0))) *
5
}s"
repeatCount="indefinite" repeatCount="indefinite"
rotate="auto" calcMode="linear"
> >
<mpath xlink:href="#solar" /> <mpath xlink:href="#solar" />
</animateMotion> </animateMotion>
</circle>` </circle>`
: ""} : ""}
${totalGridConsumption ${gridConsumption
? svg`<circle ? svg`<circle
r="1" r="1"
class="grid" class="grid"
vector-effect="non-scaling-stroke" vector-effect="non-scaling-stroke"
> >
<animateMotion <animateMotion
dur="${ dur="${6 - (gridConsumption / totalLines) * 5}s"
6 -
(totalGridConsumption /
(totalGridConsumption +
(totalSolarProduction || 0))) *
5
}s"
repeatCount="indefinite" repeatCount="indefinite"
rotate="auto" calcMode="linear"
> >
<mpath xlink:href="#grid" /> <mpath xlink:href="#grid" />
</animateMotion> </animateMotion>
</circle>` </circle>`
: ""} : ""}
${solarToBattery
? svg`<circle
r="1"
class="battery-solar"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="${6 - (solarToBattery / totalLines) * 5}s"
repeatCount="indefinite"
calcMode="linear"
>
<mpath xlink:href="#battery-solar" />
</animateMotion>
</circle>`
: ""}
${batteryConsumption
? svg`<circle
r="1"
class="battery-house"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="${6 - (batteryConsumption / totalLines) * 5}s"
repeatCount="indefinite"
calcMode="linear"
>
<mpath xlink:href="#battery-house" />
</animateMotion>
</circle>`
: ""}
${batteryFromGrid
? svg`<circle
r="1"
class="battery-from-grid"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="${6 - (batteryFromGrid / totalLines) * 5}s"
repeatCount="indefinite"
keyPoints="1;0" keyTimes="0;1"
calcMode="linear"
>
<mpath xlink:href="#battery-grid" />
</animateMotion>
</circle>`
: ""}
${batteryToGrid
? svg`<circle
r="1"
class="battery-to-grid"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="${6 - (batteryToGrid / totalLines) * 5}s"
repeatCount="indefinite"
calcMode="linear"
>
<mpath xlink:href="#battery-grid" />
</animateMotion>
</circle>`
: ""}
</svg> </svg>
</div> </div>
</div> </div>
@ -433,6 +643,10 @@ class HuiEnergyDistrubutionCard
padding: 0 16px 16px; padding: 0 16px 16px;
box-sizing: border-box; box-sizing: border-box;
} }
.lines.battery {
bottom: 100px;
height: 156px;
}
.lines svg { .lines svg {
width: calc(100% - 160px); width: calc(100% - 160px);
height: 100%; height: 100%;
@ -456,6 +670,10 @@ class HuiEnergyDistrubutionCard
margin-left: 4px; margin-left: 4px;
height: 130px; height: 130px;
} }
.circle-container.battery {
height: 110px;
justify-content: flex-end;
}
.spacer { .spacer {
width: 84px; width: 84px;
} }
@ -523,11 +741,48 @@ class HuiEnergyDistrubutionCard
stroke-width: 4; stroke-width: 4;
fill: var(--energy-solar-color); fill: var(--energy-solar-color);
} }
path.return, .battery .circle {
circle.return { border-color: var(--energy-battery-in-color);
}
circle.battery,
path.battery {
stroke: var(--energy-battery-out-color);
}
path.battery-house,
circle.battery-house {
stroke: var(--energy-battery-out-color);
}
circle.battery-house {
stroke-width: 4;
fill: var(--energy-battery-out-color);
}
path.battery-solar,
circle.battery-solar {
stroke: var(--energy-battery-in-color);
}
circle.battery-solar {
stroke-width: 4;
fill: var(--energy-battery-in-color);
}
.battery-in {
color: var(--energy-battery-in-color);
}
.battery-out {
color: var(--energy-battery-out-color);
}
path.battery-from-grid {
stroke: var(--energy-grid-consumption-color);
}
path.battery-to-grid {
stroke: var(--energy-grid-return-color); stroke: var(--energy-grid-return-color);
} }
circle.return { path.return,
circle.return,
circle.battery-to-grid {
stroke: var(--energy-grid-return-color);
}
circle.return,
circle.battery-to-grid {
stroke-width: 4; stroke-width: 4;
fill: var(--energy-grid-return-color); fill: var(--energy-grid-return-color);
} }
@ -541,10 +796,12 @@ class HuiEnergyDistrubutionCard
color: var(--energy-grid-consumption-color); color: var(--energy-grid-consumption-color);
} }
circle.grid, circle.grid,
circle.battery-from-grid,
path.grid { path.grid {
stroke: var(--energy-grid-consumption-color); stroke: var(--energy-grid-consumption-color);
} }
circle.grid { circle.grid,
circle.battery-from-grid {
stroke-width: 4; stroke-width: 4;
fill: var(--energy-grid-consumption-color); fill: var(--energy-grid-consumption-color);
} }

View File

@ -73,6 +73,7 @@ export class HuiEnergySourcesTableCard
let totalGrid = 0; let totalGrid = 0;
let totalSolar = 0; let totalSolar = 0;
let totalBattery = 0;
let totalCost = 0; let totalCost = 0;
const types = energySourcesByType(this._data.prefs); const types = energySourcesByType(this._data.prefs);
@ -81,6 +82,12 @@ export class HuiEnergySourcesTableCard
const solarColor = computedStyles const solarColor = computedStyles
.getPropertyValue("--energy-solar-color") .getPropertyValue("--energy-solar-color")
.trim(); .trim();
const batteryFromColor = computedStyles
.getPropertyValue("--energy-battery-out-color")
.trim();
const batteryToColor = computedStyles
.getPropertyValue("--energy-battery-in-color")
.trim();
const returnColor = computedStyles const returnColor = computedStyles
.getPropertyValue("--energy-grid-return-color") .getPropertyValue("--energy-grid-return-color")
.trim(); .trim();
@ -190,6 +197,99 @@ export class HuiEnergySourcesTableCard
: ""} : ""}
</tr>` </tr>`
: ""} : ""}
${types.battery?.map((source, idx) => {
const entityFrom = this.hass.states[source.stat_energy_from];
const entityTo = this.hass.states[source.stat_energy_to];
const energyFrom =
calculateStatisticSumGrowth(
this._data!.stats[source.stat_energy_from]
) || 0;
const energyTo =
calculateStatisticSumGrowth(
this._data!.stats[source.stat_energy_to]
) || 0;
totalBattery += energyFrom - energyTo;
const fromColor =
idx > 0
? rgb2hex(
lab2rgb(
labDarken(rgb2lab(hex2rgb(batteryFromColor)), idx)
)
)
: batteryFromColor;
const toColor =
idx > 0
? rgb2hex(
lab2rgb(
labDarken(rgb2lab(hex2rgb(batteryToColor)), idx)
)
)
: batteryToColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
class="bullet"
style=${styleMap({
borderColor: fromColor,
backgroundColor: fromColor + "7F",
})}
></div>
</td>
<th class="mdc-data-table__cell" scope="row">
${entityFrom
? computeStateName(entityFrom)
: source.stat_energy_from}
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(energyFrom, this.hass.locale)} kWh
</td>
${showCosts
? html`<td class="mdc-data-table__cell"></td>`
: ""}
</tr>
<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
class="bullet"
style=${styleMap({
borderColor: toColor,
backgroundColor: toColor + "7F",
})}
></div>
</td>
<th class="mdc-data-table__cell" scope="row">
${entityTo
? computeStateName(entityTo)
: source.stat_energy_from}
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(energyTo * -1, this.hass.locale)} kWh
</td>
${showCosts
? html`<td class="mdc-data-table__cell"></td>`
: ""}
</tr>`;
})}
${types.battery
? html`<tr class="mdc-data-table__row total">
<td class="mdc-data-table__cell"></td>
<th class="mdc-data-table__cell" scope="row">
Battery total
</th>
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(totalBattery, this.hass.locale)} kWh
</td>
${showCosts
? html`<td class="mdc-data-table__cell"></td>`
: ""}
</tr>`
: ""}
${types.grid?.map( ${types.grid?.map(
(source) => html`${source.flow_from.map((flow, idx) => { (source) => html`${source.flow_from.map((flow, idx) => {
const entity = this.hass.states[flow.stat_energy_from]; const entity = this.hass.states[flow.stat_energy_from];

View File

@ -244,6 +244,8 @@ export class HuiEnergyUsageGraphCard
to_grid?: string[]; to_grid?: string[];
from_grid?: string[]; from_grid?: string[];
solar?: string[]; solar?: string[];
to_battery?: string[];
from_battery?: string[];
} = {}; } = {};
for (const source of energyData.prefs.energy_sources) { for (const source of energyData.prefs.energy_sources) {
@ -256,6 +258,17 @@ export class HuiEnergyUsageGraphCard
continue; continue;
} }
if (source.type === "battery") {
if (statistics.to_battery) {
statistics.to_battery.push(source.stat_energy_to);
statistics.from_battery!.push(source.stat_energy_from);
} else {
statistics.to_battery = [source.stat_energy_to];
statistics.from_battery = [source.stat_energy_from];
}
continue;
}
// grid source // grid source
for (const flowFrom of source.flow_from) { for (const flowFrom of source.flow_from) {
if (statistics.from_grid) { if (statistics.from_grid) {
@ -306,21 +319,47 @@ export class HuiEnergyUsageGraphCard
} }
const combinedData: { const combinedData: {
[key: string]: { [statId: string]: { [start: string]: number } }; to_grid?: { [statId: string]: { [start: string]: number } };
to_battery?: { [statId: string]: { [start: string]: number } };
from_grid?: { [statId: string]: { [start: string]: number } };
used_grid?: { [statId: string]: { [start: string]: number } };
used_solar?: { [statId: string]: { [start: string]: number } };
used_battery?: { [statId: string]: { [start: string]: number } };
} = {};
const summedData: {
to_grid?: { [start: string]: number };
from_grid?: { [start: string]: number };
to_battery?: { [start: string]: number };
from_battery?: { [start: string]: number };
solar?: { [start: string]: number };
} = {}; } = {};
const summedData: { [key: string]: { [start: string]: number } } = {};
const computedStyles = getComputedStyle(this); const computedStyles = getComputedStyle(this);
const colors = { const colors = {
to_grid: computedStyles to_grid: computedStyles
.getPropertyValue("--energy-grid-return-color") .getPropertyValue("--energy-grid-return-color")
.trim(), .trim(),
to_battery: computedStyles
.getPropertyValue("--energy-battery-in-color")
.trim(),
from_grid: computedStyles from_grid: computedStyles
.getPropertyValue("--energy-grid-consumption-color") .getPropertyValue("--energy-grid-consumption-color")
.trim(), .trim(),
used_grid: computedStyles
.getPropertyValue("--energy-grid-consumption-color")
.trim(),
used_solar: computedStyles used_solar: computedStyles
.getPropertyValue("--energy-solar-color") .getPropertyValue("--energy-solar-color")
.trim(), .trim(),
used_battery: computedStyles
.getPropertyValue("--energy-battery-out-color")
.trim(),
};
const labels = {
used_grid: "Combined from grid",
used_solar: "Consumed solar",
used_battery: "Consumed battery",
}; };
const backgroundColor = computedStyles const backgroundColor = computedStyles
@ -328,8 +367,14 @@ export class HuiEnergyUsageGraphCard
.trim(); .trim();
Object.entries(statistics).forEach(([key, statIds]) => { Object.entries(statistics).forEach(([key, statIds]) => {
const sum = ["solar", "to_grid"].includes(key); const sum = [
const add = key !== "solar"; "solar",
"to_grid",
"from_grid",
"to_battery",
"from_battery",
].includes(key);
const add = !["solar", "from_battery"].includes(key);
const totalStats: { [start: string]: number } = {}; const totalStats: { [start: string]: number } = {};
const sets: { [statId: string]: { [start: string]: number } } = {}; const sets: { [statId: string]: { [start: string]: number } } = {};
statIds!.forEach((id) => { statIds!.forEach((id) => {
@ -374,15 +419,72 @@ export class HuiEnergyUsageGraphCard
} }
}); });
if (summedData.to_grid && summedData.solar) { const grid_to_battery = {};
const battery_to_grid = {};
if ((summedData.to_grid || summedData.to_battery) && summedData.solar) {
const used_solar = {}; const used_solar = {};
for (const start of Object.keys(summedData.solar)) { for (const start of Object.keys(summedData.solar)) {
used_solar[start] = Math.max( used_solar[start] =
(summedData.solar[start] || 0) - (summedData.to_grid[start] || 0), (summedData.solar[start] || 0) -
0 (summedData.to_grid?.[start] || 0) -
); (summedData.to_battery?.[start] || 0);
if (used_solar[start] < 0) {
if (summedData.to_battery) {
grid_to_battery[start] = used_solar[start] * -1;
if (grid_to_battery[start] > (summedData.from_grid?.[start] || 0)) {
battery_to_grid[start] = Math.min(
0,
grid_to_battery[start] - (summedData.from_grid?.[start] || 0)
);
grid_to_battery[start] = summedData.from_grid?.[start];
}
}
used_solar[start] = 0;
}
} }
combinedData.used_solar = { used_solar: used_solar }; combinedData.used_solar = { used_solar };
}
if (summedData.from_battery) {
if (summedData.to_grid) {
const used_battery = {};
for (const start of Object.keys(summedData.from_battery)) {
used_battery[start] =
(summedData.from_battery![start] || 0) -
(battery_to_grid[start] || 0);
}
combinedData.used_battery = { used_battery };
} else {
combinedData.used_battery = { used_battery: summedData.from_battery };
}
}
if (combinedData.from_grid && summedData.to_battery) {
const used_grid = {};
for (const start of Object.keys(grid_to_battery)) {
let noOfSources = 0;
let source: string;
for (const [key, stats] of Object.entries(combinedData.from_grid)) {
if (stats[start]) {
source = key;
noOfSources++;
}
if (noOfSources > 1) {
break;
}
}
if (noOfSources === 1) {
combinedData.from_grid[source!][start] -= grid_to_battery[start] || 0;
} else {
let total_from_grid = 0;
Object.values(combinedData.from_grid).forEach((stats) => {
total_from_grid += stats[start] || 0;
delete stats[start];
});
used_grid[start] = total_from_grid - (grid_to_battery[start] || 0);
}
}
combinedData.used_grid = { used_grid };
} }
let allKeys: string[] = []; let allKeys: string[] = [];
@ -406,12 +508,17 @@ export class HuiEnergyUsageGraphCard
data.push({ data.push({
label: label:
type === "used_solar" type in labels
? "Consumed solar" ? labels[type]
: entity : entity
? computeStateName(entity) ? computeStateName(entity)
: statId, : statId,
order: type === "used_solar" ? 0 : idx + 1, order:
type === "used_solar"
? 0
: type === "to_battery"
? Object.keys(combinedData).length
: idx + 1,
borderColor, borderColor,
backgroundColor: hexBlend(borderColor, backgroundColor, 50), backgroundColor: hexBlend(borderColor, backgroundColor, 50),
stack: "stack", stack: "stack",
@ -425,7 +532,10 @@ export class HuiEnergyUsageGraphCard
// @ts-expect-error // @ts-expect-error
data[0].data.push({ data[0].data.push({
x: date.getTime(), x: date.getTime(),
y: value && type === "to_grid" ? -1 * value : value, y:
value && ["to_grid", "to_battery"].includes(type)
? -1 * value
: value,
}); });
} }

View File

@ -88,6 +88,8 @@ documentContainer.innerHTML = `<custom-style>
--energy-grid-return-color: #673ab7; --energy-grid-return-color: #673ab7;
--energy-solar-color: #ff9800; --energy-solar-color: #ff9800;
--energy-non-fossil-color: #0f9d58; --energy-non-fossil-color: #0f9d58;
--energy-battery-out-color: #4db6ac;
--energy-battery-in-color: #f06292;
/* opacity for dark text on a light background */ /* opacity for dark text on a light background */
--dark-divider-opacity: 0.12; --dark-divider-opacity: 0.12;

View File

@ -1046,6 +1046,11 @@
"stat_return_to_grid": "Solar energy returned to the grid", "stat_return_to_grid": "Solar energy returned to the grid",
"stat_predicted_production": "Prediction of your solar energy production" "stat_predicted_production": "Prediction of your solar energy production"
}, },
"battery": {
"title": "Home Battery Storage",
"sub": "If you have a battery system, you can configure it to monitor how much energy was stored and used from your battery.",
"learn_more": "More information on how to get started."
},
"device_consumption": { "device_consumption": {
"title": "Individual devices", "title": "Individual devices",
"sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.", "sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",