From 2cdf78c50460723dd6497d33c87f73464cd254cc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 30 Jul 2021 21:55:58 +0200 Subject: [PATCH] Add grid neutrality gauge (#9655) --- src/components/ha-gauge.ts | 8 +- .../energy/strategies/energy-strategy.ts | 20 +- .../hui-energy-grid-neutrality-gauge-card.ts | 177 ++++++++++++++++++ src/panels/lovelace/cards/types.ts | 6 + .../create-element/create-card-element.ts | 2 + 5 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts diff --git a/src/components/ha-gauge.ts b/src/components/ha-gauge.ts index 81161ed64d..25dbae2b5f 100644 --- a/src/components/ha-gauge.ts +++ b/src/components/ha-gauge.ts @@ -13,7 +13,7 @@ const getAngle = (value: number, min: number, max: number) => { return (percentage * 180) / 100; }; -interface LevelDefinition { +export interface LevelDefinition { level: number; stroke: string; } @@ -26,6 +26,8 @@ export class Gauge extends LitElement { @property({ type: Number }) public value = 0; + @property({ type: String }) public valueText?: string; + @property() public locale!: FrontendLocaleData; @property({ type: Boolean }) public needle?: boolean; @@ -131,7 +133,9 @@ export class Gauge extends LitElement { - ${formatNumber(this.value, this.locale)} ${this.label} + ${this.valueText || formatNumber(this.value, this.locale)} ${ + this.label + } `; } diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index 7177e6d9a7..325bb65ada 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -1,4 +1,8 @@ -import { EnergyPreferences, getEnergyPreferences } from "../../../data/energy"; +import { + EnergyPreferences, + getEnergyPreferences, + GridSourceTypeEnergyPreference, +} from "../../../data/energy"; import { LovelaceViewConfig } from "../../../data/lovelace"; import { LovelaceViewStrategy } from "../../lovelace/strategies/get-strategy"; @@ -39,9 +43,10 @@ export class EnergyStrategy { view.type = "sidebar"; - const hasGrid = energyPrefs.energy_sources.some( + const hasGrid = energyPrefs.energy_sources.find( (source) => source.type === "grid" - ); + ) as GridSourceTypeEnergyPreference; + const hasReturn = hasGrid && hasGrid.flow_to.length; const hasSolar = energyPrefs.energy_sources.some( (source) => source.type === "solar" ); @@ -100,6 +105,15 @@ export class EnergyStrategy { }); } + // Only include if we have a grid source & return. + if (hasReturn) { + view.cards!.push({ + type: "energy-grid-neutrality-gauge", + prefs: energyPrefs, + view_layout: { position: "sidebar" }, + }); + } + // Only include if we have a grid if (hasGrid) { view.cards!.push({ diff --git a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts new file mode 100644 index 0000000000..97f8dab168 --- /dev/null +++ b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts @@ -0,0 +1,177 @@ +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { formatNumber } from "../../../../common/string/format_number"; +import "../../../../components/ha-card"; +import "../../../../components/ha-gauge"; +import type { LevelDefinition } from "../../../../components/ha-gauge"; +import { GridSourceTypeEnergyPreference } from "../../../../data/energy"; +import { + calculateStatisticsSumGrowth, + fetchStatistics, + Statistics, +} from "../../../../data/history"; +import type { HomeAssistant } from "../../../../types"; +import type { LovelaceCard } from "../../types"; +import type { EnergyGridGaugeCardConfig } from "../types"; + +const LEVELS: LevelDefinition[] = [ + { level: -1, stroke: "var(--label-badge-red)" }, + { level: -0.2, stroke: "var(--label-badge-yellow)" }, + { level: 0, stroke: "var(--label-badge-green)" }, +]; + +@customElement("hui-energy-grid-neutrality-gauge-card") +class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { + @property({ attribute: false }) public hass?: HomeAssistant; + + @state() private _config?: EnergyGridGaugeCardConfig; + + @state() private _stats?: Statistics; + + public getCardSize(): number { + return 4; + } + + public setConfig(config: EnergyGridGaugeCardConfig): void { + this._config = config; + } + + public willUpdate(changedProps) { + super.willUpdate(changedProps); + + if (!this.hasUpdated) { + this._getStatistics(); + } + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + if (!this._stats) { + return html`Loading...`; + } + + const prefs = this._config!.prefs; + const gridSource = prefs.energy_sources.find( + (src) => src.type === "grid" + ) as GridSourceTypeEnergyPreference | undefined; + + let value: number | undefined; + + if (!gridSource) { + return html``; + } + + const consumedFromGrid = calculateStatisticsSumGrowth( + this._stats, + gridSource.flow_from.map((flow) => flow.stat_energy_from) + ); + + const returnedToGrid = calculateStatisticsSumGrowth( + this._stats, + gridSource.flow_to.map((flow) => flow.stat_energy_to) + ); + + if (consumedFromGrid !== null && returnedToGrid !== null) { + if (returnedToGrid > consumedFromGrid) { + value = 1 - consumedFromGrid / returnedToGrid; + } else if (returnedToGrid < consumedFromGrid) { + value = (1 - returnedToGrid / consumedFromGrid) * -1; + } else { + value = 0; + } + } + + return html` + + ${value !== undefined + ? html` +
+ ${returnedToGrid! >= consumedFromGrid! + ? "Returned to the grid" + : "Consumed from the grid"} +
` + : "Grid neutrality could not be calculated"} +
+ `; + } + + private async _getStatistics(): Promise { + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint + + const statistics: string[] = []; + const prefs = this._config!.prefs; + for (const source of prefs.energy_sources) { + if (source.type === "solar") { + continue; + } + + // grid source + for (const flowFrom of source.flow_from) { + statistics.push(flowFrom.stat_energy_from); + } + for (const flowTo of source.flow_to) { + statistics.push(flowTo.stat_energy_to); + } + } + + this._stats = await fetchStatistics( + this.hass!, + startDate, + undefined, + statistics + ); + } + + 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; + } + + .name { + text-align: center; + line-height: initial; + color: var(--primary-text-color); + width: 100%; + font-size: 15px; + margin-top: 8px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-energy-grid-neutrality-gauge-card": HuiEnergyGridGaugeCard; + } +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 5d191334a3..4894837543 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -131,6 +131,12 @@ export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { prefs: EnergyPreferences; } +export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { + type: "energy-grid-result-gauge"; + title?: string; + prefs: EnergyPreferences; +} + export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig { type: "energy-carbon-consumed-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 f6a489c098..e73a60619d 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -47,6 +47,8 @@ const LAZY_LOAD_TYPES = { import("../cards/energy/hui-energy-distribution-card"), "energy-solar-consumed-gauge": () => import("../cards/energy/hui-energy-solar-consumed-gauge-card"), + "energy-grid-neutrality-gauge": () => + import("../cards/energy/hui-energy-grid-neutrality-gauge-card"), "energy-carbon-consumed-gauge": () => import("../cards/energy/hui-energy-carbon-consumed-gauge-card"), grid: () => import("../cards/hui-grid-card"),