From 19e4c0657aacfd17987d436bed31279e5517f169 Mon Sep 17 00:00:00 2001
From: Bram Kragten
Date: Thu, 12 Aug 2021 17:40:21 +0200
Subject: [PATCH] Add battery to energy dashboard (#9757)
---
demo/src/stubs/energy.ts | 5 +
demo/src/stubs/entities.ts | 18 +
src/data/energy.ts | 23 +-
.../components/ha-energy-battery-settings.ts | 177 ++++++++
.../dialogs/dialog-energy-battery-settings.ts | 132 ++++++
.../dialogs/dialog-energy-solar-settings.ts | 1 -
.../energy/dialogs/show-dialogs-energy.ts | 17 +
src/panels/config/energy/ha-config-energy.ts | 6 +
.../energy/cards/energy-setup-wizard-card.ts | 15 +-
.../energy/hui-energy-distribution-card.ts | 413 ++++++++++++++----
.../energy/hui-energy-sources-table-card.ts | 100 +++++
.../energy/hui-energy-usage-graph-card.ts | 138 +++++-
src/resources/ha-style.ts | 2 +
src/translations/en.json | 5 +
14 files changed, 954 insertions(+), 98 deletions(-)
create mode 100644 src/panels/config/energy/components/ha-energy-battery-settings.ts
create mode 100644 src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts
diff --git a/demo/src/stubs/energy.ts b/demo/src/stubs/energy.ts
index 58b7768b61..80af3d1ea1 100644
--- a/demo/src/stubs/energy.ts
+++ b/demo/src/stubs/energy.ts
@@ -44,6 +44,11 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
stat_energy_from: "sensor.solar_production",
config_entry_solar_forecast: ["solar_forecast"],
},
+ {
+ type: "battery",
+ stat_energy_from: "sensor.battery_output",
+ stat_energy_to: "sensor.battery_input",
+ },
],
device_consumption: [
{
diff --git a/demo/src/stubs/entities.ts b/demo/src/stubs/entities.ts
index 6ebfae96dc..df7075e161 100644
--- a/demo/src/stubs/entities.ts
+++ b/demo/src/stubs/entities.ts
@@ -18,6 +18,24 @@ export const energyEntities = () =>
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": {
entity_id: "sensor.energy_consumption_tarif_1 ",
state: "88.6",
diff --git a/src/data/energy.ts b/src/data/energy.ts
index a35dded6d1..055e61cadb 100644
--- a/src/data/energy.ts
+++ b/src/data/energy.ts
@@ -47,6 +47,13 @@ export const emptySolarEnergyPreference =
config_entry_solar_forecast: null,
});
+export const emptyBatteryEnergyPreference =
+ (): BatterySourceTypeEnergyPreference => ({
+ type: "battery",
+ stat_energy_from: "",
+ stat_energy_to: "",
+ });
+
export interface DeviceConsumptionEnergyPreference {
// This is an ever increasing value
stat_consumption: string;
@@ -94,9 +101,16 @@ export interface SolarSourceTypeEnergyPreference {
config_entry_solar_forecast: string[] | null;
}
+export interface BatterySourceTypeEnergyPreference {
+ type: "battery";
+ stat_energy_from: string;
+ stat_energy_to: string;
+}
+
type EnergySource =
| SolarSourceTypeEnergyPreference
- | GridSourceTypeEnergyPreference;
+ | GridSourceTypeEnergyPreference
+ | BatterySourceTypeEnergyPreference;
export interface EnergyPreferences {
energy_sources: EnergySource[];
@@ -132,6 +146,7 @@ export const saveEnergyPreferences = async (
interface EnergySourceByType {
grid?: GridSourceTypeEnergyPreference[];
solar?: SolarSourceTypeEnergyPreference[];
+ battery?: BatterySourceTypeEnergyPreference[];
}
export const energySourcesByType = (prefs: EnergyPreferences) => {
@@ -203,6 +218,12 @@ const getEnergyData = async (
continue;
}
+ if (source.type === "battery") {
+ statIDs.push(source.stat_energy_from);
+ statIDs.push(source.stat_energy_to);
+ continue;
+ }
+
// grid source
for (const flowFrom of source.flow_from) {
statIDs.push(flowFrom.stat_energy_from);
diff --git a/src/panels/config/energy/components/ha-energy-battery-settings.ts b/src/panels/config/energy/components/ha-energy-battery-settings.ts
new file mode 100644
index 0000000000..9caa3b8e60
--- /dev/null
+++ b/src/panels/config/energy/components/ha-energy-battery-settings.ts
@@ -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`
+
+
+
+
+
+ ${this.hass.localize("ui.panel.config.energy.battery.sub")}
+ ${this.hass.localize(
+ "ui.panel.config.energy.battery.learn_more"
+ )}
+
+
Battery systems
+ ${batterySources.map((source) => {
+ const fromEntityState = this.hass.states[source.stat_energy_from];
+ const toEntityState = this.hass.states[source.stat_energy_to];
+ return html`
+
+ ${toEntityState?.attributes.icon
+ ? html`
`
+ : html`
`}
+
+ ${toEntityState
+ ? computeStateName(toEntityState)
+ : source.stat_energy_from}
+ ${fromEntityState
+ ? computeStateName(fromEntityState)
+ : source.stat_energy_to}
+
+
+
+
+
+
+
+
+ `;
+ })}
+
+
+ Add battery system
+
+
+
+ `;
+ }
+
+ 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;
+ }
+}
diff --git a/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts
new file mode 100644
index 0000000000..20eda2eead
--- /dev/null
+++ b/src/panels/config/energy/dialogs/dialog-energy-battery-settings.ts
@@ -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
+{
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _params?: EnergySettingsBatteryDialogParams;
+
+ @state() private _source?: BatterySourceTypeEnergyPreference;
+
+ @state() private _error?: string;
+
+ public async showDialog(
+ params: EnergySettingsBatteryDialogParams
+ ): Promise {
+ 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`
+
+ Configure battery system`}
+ @closed=${this.closeDialog}
+ >
+ ${this._error ? html`${this._error}
` : ""}
+
+
+
+
+
+
+ ${this.hass.localize("ui.common.cancel")}
+
+
+ ${this.hass.localize("ui.common.save")}
+
+
+ `;
+ }
+
+ 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;
+ }
+}
diff --git a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts
index 9d6501f1bf..3cb983a857 100644
--- a/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts
+++ b/src/panels/config/energy/dialogs/dialog-energy-solar-settings.ts
@@ -17,7 +17,6 @@ import "../../../../components/ha-radio";
import "../../../../components/ha-checkbox";
import type { HaCheckbox } from "../../../../components/ha-checkbox";
import "../../../../components/ha-formfield";
-import "../../../../components/entity/ha-entity-picker";
import type { HaRadio } from "../../../../components/ha-radio";
import { showConfigFlowDialog } from "../../../../dialogs/config-flow/show-dialog-config-flow";
import { ConfigEntry, getConfigEntries } from "../../../../data/config_entries";
diff --git a/src/panels/config/energy/dialogs/show-dialogs-energy.ts b/src/panels/config/energy/dialogs/show-dialogs-energy.ts
index f471cce7a7..718a09791a 100644
--- a/src/panels/config/energy/dialogs/show-dialogs-energy.ts
+++ b/src/panels/config/energy/dialogs/show-dialogs-energy.ts
@@ -1,5 +1,6 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import {
+ BatterySourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
FlowFromGridSourceEnergyPreference,
FlowToGridSourceEnergyPreference,
@@ -33,6 +34,11 @@ export interface EnergySettingsSolarDialogParams {
saveCallback: (source: SolarSourceTypeEnergyPreference) => Promise;
}
+export interface EnergySettingsBatteryDialogParams {
+ source?: BatterySourceTypeEnergyPreference;
+ saveCallback: (source: BatterySourceTypeEnergyPreference) => Promise;
+}
+
export interface EnergySettingsDeviceDialogParams {
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise;
}
@@ -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 = (
element: HTMLElement,
dialogParams: EnergySettingsSolarDialogParams
diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts
index fab928a23d..1520ab3848 100644
--- a/src/panels/config/energy/ha-config-energy.ts
+++ b/src/panels/config/energy/ha-config-energy.ts
@@ -10,6 +10,7 @@ import { configSections } from "../ha-panel-config";
import "./components/ha-energy-device-settings";
import "./components/ha-energy-grid-settings";
import "./components/ha-energy-solar-settings";
+import "./components/ha-energy-battery-settings";
const INITIAL_CONFIG: EnergyPreferences = {
energy_sources: [],
@@ -81,6 +82,11 @@ class HaConfigEnergy extends LitElement {
.preferences=${this._preferences!}
@value-changed=${this._prefsChanged}
>
+
Step ${this._step + 1} of 3
${this._step === 0
- ? html` `
: this._step === 1
- ? html` `
- : html` `
+ : html`${this.hass.localize("ui.panel.energy.setup.back")}`
: html``}
- ${this._step < 2
+ ${this._step < 3
? html`${this.hass.localize("ui.panel.energy.setup.next")}`
diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts
index 78b13d914c..fedfe067cf 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts
@@ -1,6 +1,9 @@
import {
+ mdiArrowDown,
mdiArrowLeft,
mdiArrowRight,
+ mdiArrowUp,
+ mdiBatteryHigh,
mdiHome,
mdiLeaf,
mdiSolarPower,
@@ -75,9 +78,10 @@ class HuiEnergyDistrubutionCard
const hasConsumption = true;
const hasSolarProduction = types.solar !== undefined;
+ const hasBattery = types.battery !== undefined;
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
- const totalGridConsumption =
+ const totalFromGrid =
calculateStatisticsSumGrowth(
this._data.stats,
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
@@ -93,30 +97,97 @@ class HuiEnergyDistrubutionCard
) || 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) {
- productionReturnedToGrid =
+ returnedToGrid =
calculateStatisticsSumGrowth(
this._data.stats,
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
) || 0;
}
- const solarConsumption = Math.max(
- 0,
- (totalSolarProduction || 0) - (productionReturnedToGrid || 0)
- );
+ let solarConsumption: number | null = null;
+ if (hasSolarProduction) {
+ 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;
if (hasSolarProduction) {
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 homeHighCarbonCircumference: number | undefined;
@@ -129,7 +200,7 @@ class HuiEnergyDistrubutionCard
this._data.co2SignalEntity in this._data.stats
) {
// Calculate high carbon consumption
- const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage(
+ const highCarbonEnergy = calculateStatisticsSumGrowthWithPercentage(
this._data.stats[this._data.co2SignalEntity],
types
.grid![0].flow_from.map(
@@ -144,8 +215,17 @@ class HuiEnergyDistrubutionCard
electricityMapUrl += `/zone/${co2State.attributes.country_code}`;
}
- if (highCarbonConsumption !== null) {
- lowCarbonConsumption = totalGridConsumption - highCarbonConsumption;
+ if (highCarbonEnergy !== null) {
+ 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 =
CIRCLE_CIRCUMFERENCE * (highCarbonConsumption / totalHomeConsumption);
@@ -153,16 +233,26 @@ class HuiEnergyDistrubutionCard
homeLowCarbonCircumference =
CIRCLE_CIRCUMFERENCE -
(homeSolarCircumference || 0) -
+ (homeBatteryCircumference || 0) -
homeHighCarbonCircumference;
}
}
+ const totalLines =
+ gridConsumption +
+ (solarConsumption || 0) +
+ (returnedToGrid ? returnedToGrid - (batteryToGrid || 0) : 0) +
+ (solarToBattery || 0) +
+ (batteryConsumption || 0) +
+ (batteryFromGrid || 0) +
+ (batteryToGrid || 0);
+
return html`
- ${lowCarbonConsumption !== undefined || hasSolarProduction
+ ${lowCarbonEnergy !== undefined || hasSolarProduction
? html`
- ${lowCarbonConsumption === undefined
+ ${lowCarbonEnergy === undefined
? html`
`
: html`
Non-fossil
@@ -173,12 +263,10 @@ class HuiEnergyDistrubutionCard
rel="noopener no referrer"
>
- ${lowCarbonConsumption
- ? formatNumber(
- lowCarbonConsumption,
- this.hass.locale,
- { maximumFractionDigits: 1 }
- )
+ ${lowCarbonEnergy
+ ? formatNumber(lowCarbonEnergy, this.hass.locale, {
+ maximumFractionDigits: 1,
+ })
: "-"}
kWh
@@ -207,33 +295,29 @@ class HuiEnergyDistrubutionCard
+ ${returnedToGrid !== null
+ ? html`
+ ${formatNumber(returnedToGrid, this.hass.locale, {
+ maximumFractionDigits: 1,
+ })}
+ kWh
+ `
+ : ""}
${hasReturnToGrid
? html``
- : ""}${formatNumber(
- totalGridConsumption,
- this.hass.locale,
- { maximumFractionDigits: 1 }
- )}
+ : ""}${formatNumber(totalFromGrid, this.hass.locale, {
+ maximumFractionDigits: 1,
+ })}
kWh
- ${productionReturnedToGrid !== null
- ? html`
- ${formatNumber(
- productionReturnedToGrid,
- this.hass.locale,
- { maximumFractionDigits: 1 }
- )}
- kWh
- `
- : ""}
Grid
@@ -268,6 +352,23 @@ class HuiEnergyDistrubutionCard
}"
/>`
: ""}
+ ${homeBatteryCircumference
+ ? svg`
`
+ : ""}
${homeLowCarbonCircumference
? svg`
@@ -305,7 +409,39 @@ class HuiEnergyDistrubutionCard
Home
-
+ ${hasBattery
+ ? html`
+
+
+
+
+
+ ${formatNumber(totalBatteryIn || 0, this.hass.locale, {
+ maximumFractionDigits: 1,
+ })}
+ kWh
+
+
+ ${formatNumber(totalBatteryOut || 0, this.hass.locale, {
+ maximumFractionDigits: 1,
+ })}
+ kWh
+
+
Battery
+
+
+
`
+ : ""}
+
@@ -433,6 +643,10 @@ class HuiEnergyDistrubutionCard
padding: 0 16px 16px;
box-sizing: border-box;
}
+ .lines.battery {
+ bottom: 100px;
+ height: 156px;
+ }
.lines svg {
width: calc(100% - 160px);
height: 100%;
@@ -456,6 +670,10 @@ class HuiEnergyDistrubutionCard
margin-left: 4px;
height: 130px;
}
+ .circle-container.battery {
+ height: 110px;
+ justify-content: flex-end;
+ }
.spacer {
width: 84px;
}
@@ -523,11 +741,48 @@ class HuiEnergyDistrubutionCard
stroke-width: 4;
fill: var(--energy-solar-color);
}
- path.return,
- circle.return {
+ .battery .circle {
+ 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);
}
- 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;
fill: var(--energy-grid-return-color);
}
@@ -541,10 +796,12 @@ class HuiEnergyDistrubutionCard
color: var(--energy-grid-consumption-color);
}
circle.grid,
+ circle.battery-from-grid,
path.grid {
stroke: var(--energy-grid-consumption-color);
}
- circle.grid {
+ circle.grid,
+ circle.battery-from-grid {
stroke-width: 4;
fill: var(--energy-grid-consumption-color);
}
diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts
index 7df70cce72..79e7699a89 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts
@@ -73,6 +73,7 @@ export class HuiEnergySourcesTableCard
let totalGrid = 0;
let totalSolar = 0;
+ let totalBattery = 0;
let totalCost = 0;
const types = energySourcesByType(this._data.prefs);
@@ -81,6 +82,12 @@ export class HuiEnergySourcesTableCard
const solarColor = computedStyles
.getPropertyValue("--energy-solar-color")
.trim();
+ const batteryFromColor = computedStyles
+ .getPropertyValue("--energy-battery-out-color")
+ .trim();
+ const batteryToColor = computedStyles
+ .getPropertyValue("--energy-battery-in-color")
+ .trim();
const returnColor = computedStyles
.getPropertyValue("--energy-grid-return-color")
.trim();
@@ -190,6 +197,99 @@ export class HuiEnergySourcesTableCard
: ""}
`
: ""}
+ ${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`
+
+
+ |
+
+ ${entityFrom
+ ? computeStateName(entityFrom)
+ : source.stat_energy_from}
+ |
+
+ ${formatNumber(energyFrom, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html` | `
+ : ""}
+
+
+
+
+ |
+
+ ${entityTo
+ ? computeStateName(entityTo)
+ : source.stat_energy_from}
+ |
+
+ ${formatNumber(energyTo * -1, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html` | `
+ : ""}
+
`;
+ })}
+ ${types.battery
+ ? html`
+ |
+
+ Battery total
+ |
+
+ ${formatNumber(totalBattery, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html` | `
+ : ""}
+
`
+ : ""}
${types.grid?.map(
(source) => html`${source.flow_from.map((flow, idx) => {
const entity = this.hass.states[flow.stat_energy_from];
diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts
index 131efb96a6..94b4a388e5 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts
@@ -244,6 +244,8 @@ export class HuiEnergyUsageGraphCard
to_grid?: string[];
from_grid?: string[];
solar?: string[];
+ to_battery?: string[];
+ from_battery?: string[];
} = {};
for (const source of energyData.prefs.energy_sources) {
@@ -256,6 +258,17 @@ export class HuiEnergyUsageGraphCard
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
for (const flowFrom of source.flow_from) {
if (statistics.from_grid) {
@@ -306,21 +319,47 @@ export class HuiEnergyUsageGraphCard
}
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 colors = {
to_grid: computedStyles
.getPropertyValue("--energy-grid-return-color")
.trim(),
+ to_battery: computedStyles
+ .getPropertyValue("--energy-battery-in-color")
+ .trim(),
from_grid: computedStyles
.getPropertyValue("--energy-grid-consumption-color")
.trim(),
+ used_grid: computedStyles
+ .getPropertyValue("--energy-grid-consumption-color")
+ .trim(),
used_solar: computedStyles
.getPropertyValue("--energy-solar-color")
.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
@@ -328,8 +367,14 @@ export class HuiEnergyUsageGraphCard
.trim();
Object.entries(statistics).forEach(([key, statIds]) => {
- const sum = ["solar", "to_grid"].includes(key);
- const add = key !== "solar";
+ const sum = [
+ "solar",
+ "to_grid",
+ "from_grid",
+ "to_battery",
+ "from_battery",
+ ].includes(key);
+ const add = !["solar", "from_battery"].includes(key);
const totalStats: { [start: string]: number } = {};
const sets: { [statId: string]: { [start: string]: number } } = {};
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 = {};
for (const start of Object.keys(summedData.solar)) {
- used_solar[start] = Math.max(
- (summedData.solar[start] || 0) - (summedData.to_grid[start] || 0),
- 0
- );
+ used_solar[start] =
+ (summedData.solar[start] || 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[] = [];
@@ -406,12 +508,17 @@ export class HuiEnergyUsageGraphCard
data.push({
label:
- type === "used_solar"
- ? "Consumed solar"
+ type in labels
+ ? labels[type]
: entity
? computeStateName(entity)
: statId,
- order: type === "used_solar" ? 0 : idx + 1,
+ order:
+ type === "used_solar"
+ ? 0
+ : type === "to_battery"
+ ? Object.keys(combinedData).length
+ : idx + 1,
borderColor,
backgroundColor: hexBlend(borderColor, backgroundColor, 50),
stack: "stack",
@@ -425,7 +532,10 @@ export class HuiEnergyUsageGraphCard
// @ts-expect-error
data[0].data.push({
x: date.getTime(),
- y: value && type === "to_grid" ? -1 * value : value,
+ y:
+ value && ["to_grid", "to_battery"].includes(type)
+ ? -1 * value
+ : value,
});
}
diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts
index d8bb070271..1cf30e1054 100644
--- a/src/resources/ha-style.ts
+++ b/src/resources/ha-style.ts
@@ -88,6 +88,8 @@ documentContainer.innerHTML = `
--energy-grid-return-color: #673ab7;
--energy-solar-color: #ff9800;
--energy-non-fossil-color: #0f9d58;
+ --energy-battery-out-color: #4db6ac;
+ --energy-battery-in-color: #f06292;
/* opacity for dark text on a light background */
--dark-divider-opacity: 0.12;
diff --git a/src/translations/en.json b/src/translations/en.json
index 1f4ab1970e..ff9df30afb 100755
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -1046,6 +1046,11 @@
"stat_return_to_grid": "Solar energy returned to the grid",
"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": {
"title": "Individual devices",
"sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",