diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index de5d6aca44..42b0c6678a 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -141,6 +141,11 @@ export class EnergyStrategy { view_layout: { position: "sidebar" }, collection_key: "energy_dashboard", }); + view.cards!.push({ + type: "energy-self-sufficiency-gauge", + view_layout: { position: "sidebar" }, + collection_key: "energy_dashboard", + }); } // Only include if we have a grid diff --git a/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts new file mode 100644 index 0000000000..d1b3221f6f --- /dev/null +++ b/src/panels/lovelace/cards/energy/hui-energy-self-sufficiency-gauge-card.ts @@ -0,0 +1,257 @@ +import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; +import { mdiInformation } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import "../../../../components/ha-card"; +import "../../../../components/ha-gauge"; +import "../../../../components/ha-svg-icon"; +import { + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; +import { calculateStatisticsSumGrowth } from "../../../../data/recorder"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import type { HomeAssistant } from "../../../../types"; +import type { LovelaceCard } from "../../types"; +import { severityMap } from "../hui-gauge-card"; +import type { EnergySelfSufficiencyGaugeCardConfig } from "../types"; + +@customElement("hui-energy-self-sufficiency-gauge-card") +class HuiEnergySelfSufficiencyGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EnergySelfSufficiencyGaugeCardConfig; + + @state() private _data?: EnergyData; + + protected hassSubscribeRequiredHostProps = ["_config"]; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass!, { + key: this._config?.collection_key, + }).subscribe((data) => { + this._data = data; + }), + ]; + } + + public getCardSize(): number { + return 4; + } + + public setConfig(config: EnergySelfSufficiencyGaugeCardConfig): void { + this._config = config; + } + + protected render() { + if (!this._config || !this.hass) { + return nothing; + } + + if (!this._data) { + return html`${this.hass.localize( + "ui.panel.lovelace.cards.energy.loading" + )}`; + } + + const prefs = this._data.prefs; + const types = energySourcesByType(prefs); + + // The strategy only includes this card if we have a grid. + const hasConsumption = true; + + const hasSolarProduction = types.solar !== undefined; + const hasBattery = types.battery !== undefined; + const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; + + const totalFromGrid = + calculateStatisticsSumGrowth( + this._data.stats, + types.grid![0].flow_from.map((flow) => flow.stat_energy_from) + ) ?? 0; + + let totalSolarProduction: number | null = null; + + if (hasSolarProduction) { + totalSolarProduction = + calculateStatisticsSumGrowth( + this._data.stats, + types.solar!.map((source) => source.stat_energy_from) + ) || 0; + } + + 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) { + returnedToGrid = + calculateStatisticsSumGrowth( + this._data.stats, + types.grid![0].flow_to.map((flow) => flow.stat_energy_to) + ) || 0; + } + + let solarConsumption: number | null = null; + if (hasSolarProduction) { + solarConsumption = + (totalSolarProduction || 0) - + (returnedToGrid || 0) - + (totalBatteryIn || 0); + } + + 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 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 value: number | undefined; + if ( + totalFromGrid !== null && + totalHomeConsumption !== null && + totalHomeConsumption > 0 + ) { + value = (1 - totalFromGrid / totalHomeConsumption) * 100; + } + + return html` + + ${value !== undefined + ? html` + + + + ${this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.card_indicates_self_sufficiency_quota" + )} + + + +
+ ${this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_quota" + )} +
+ ` + : this.hass.localize( + "ui.panel.lovelace.cards.energy.self_sufficiency_gauge.self_sufficiency_could_not_calc" + )} +
+ `; + } + + private _computeSeverity(numberValue: number): string { + if (numberValue > 75) { + return severityMap.green; + } + if (numberValue < 50) { + return severityMap.yellow; + } + return severityMap.normal; + } + + static get styles(): CSSResultGroup { + return css` + ha-card { + height: 100%; + overflow: hidden; + padding: 16px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + box-sizing: border-box; + } + + ha-gauge { + width: 100%; + max-width: 250px; + direction: ltr; + } + + .name { + text-align: center; + line-height: initial; + color: var(--primary-text-color); + width: 100%; + font-size: 15px; + margin-top: 8px; + } + + ha-svg-icon { + position: absolute; + right: 4px; + top: 4px; + color: var(--secondary-text-color); + } + simple-tooltip > span { + font-size: 12px; + line-height: 12px; + } + simple-tooltip { + width: 80%; + max-width: 250px; + top: 8px !important; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-energy-self-sufficiency-gauge-card": HuiEnergySelfSufficiencyGaugeCard; + } +} diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index 0365164fb7..4f5e6cbfdf 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -68,10 +68,11 @@ class HuiEnergySolarGaugeCard return nothing; } - const totalSolarProduction = calculateStatisticsSumGrowth( - this._data.stats, - types.solar.map((source) => source.stat_energy_from) - ); + const totalSolarProduction = + calculateStatisticsSumGrowth( + this._data.stats, + types.solar.map((source) => source.stat_energy_from) + ) || 0; const productionReturnedToGrid = calculateStatisticsSumGrowth( this._data.stats, diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 0e65d9a46a..fac076ad93 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -159,6 +159,13 @@ export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { collection_key?: string; } +export interface EnergySelfSufficiencyGaugeCardConfig + extends LovelaceCardConfig { + type: "energy-self-sufficiency-gauge"; + title?: string; + collection_key?: string; +} + export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { type: "energy-grid-result-gauge"; title?: string; diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 98390dc9bf..3ecd7b4a16 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -53,6 +53,8 @@ const LAZY_LOAD_TYPES = { import("../cards/energy/hui-energy-grid-neutrality-gauge-card"), "energy-solar-consumed-gauge": () => import("../cards/energy/hui-energy-solar-consumed-gauge-card"), + "energy-self-sufficiency-gauge": () => + import("../cards/energy/hui-energy-self-sufficiency-gauge-card"), "energy-solar-graph": () => import("../cards/energy/hui-energy-solar-graph-card"), "energy-sources-table": () => diff --git a/src/translations/en.json b/src/translations/en.json old mode 100755 new mode 100644 index 50ceeab10f..25900e1209 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4219,6 +4219,11 @@ "not_produced_solar_energy": "You have not produced any solar energy", "self_consumed_solar_could_not_calc": "Self-consumed solar energy couldn't be calculated" }, + "self_sufficiency_gauge": { + "card_indicates_self_sufficiency_quota": "This card indicates how self-sufficient your home is.", + "self_sufficiency_quota": "Self-sufficiency quota", + "self_sufficiency_could_not_calc": "Self-sufficiency quota couldn't be calculated" + }, "grid_neutrality_gauge": { "energy_dependency": "This card indicates your net energy usage.", "color_explain": "If the needle is in the purple, you returned more energy to the grid than you consumed from it. If it's in the blue, you consumed more energy from the grid than you returned.",