diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 9dd4889aa2..c662823f58 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -272,7 +272,7 @@ export default class HaChartBase extends LitElement { border-radius: 50%; display: inline-block; height: 16px; - margin-right: 4px; + margin-right: 6px; width: 16px; flex-shrink: 0; box-sizing: border-box; @@ -280,9 +280,10 @@ export default class HaChartBase extends LitElement { .chartTooltip .bullet { align-self: baseline; } + :host([rtl]) .chartLegend .bullet, :host([rtl]) .chartTooltip .bullet { margin-right: inherit; - margin-left: 4px; + margin-left: 6px; } .chartTooltip { padding: 8px; @@ -314,6 +315,7 @@ export default class HaChartBase extends LitElement { white-space: pre-line; align-items: center; line-height: 16px; + padding: 4px 0; } .chartTooltip .title { text-align: center; diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index 7e19dbfd3a..0f63492569 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -85,7 +85,8 @@ export class EnergyStrategy { // Only include if we have a grid. if (hasGrid) { view.cards!.push({ - type: "energy-usage", + title: "Energy distribution", + type: "energy-distribution", prefs: energyPrefs, view_layout: { position: "sidebar" }, }); diff --git a/src/panels/lovelace/cards/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts similarity index 83% rename from src/panels/lovelace/cards/hui-energy-carbon-consumed-gauge-card.ts rename to src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index a5fb506d85..4885489c47 100644 --- a/src/panels/lovelace/cards/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -1,23 +1,23 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; -import { round } from "../../../common/number/round"; -import { subscribeOne } from "../../../common/util/subscribe-one"; -import "../../../components/ha-card"; -import "../../../components/ha-gauge"; -import { getConfigEntries } from "../../../data/config_entries"; -import { energySourcesByType } from "../../../data/energy"; -import { subscribeEntityRegistry } from "../../../data/entity_registry"; +import { round } from "../../../../common/number/round"; +import { subscribeOne } from "../../../../common/util/subscribe-one"; +import "../../../../components/ha-card"; +import "../../../../components/ha-gauge"; +import { getConfigEntries } from "../../../../data/config_entries"; +import { energySourcesByType } from "../../../../data/energy"; +import { subscribeEntityRegistry } from "../../../../data/entity_registry"; import { calculateStatisticsSumGrowth, fetchStatistics, Statistics, -} from "../../../data/history"; -import type { HomeAssistant } from "../../../types"; -import { createEntityNotFoundWarning } from "../components/hui-warning"; -import type { LovelaceCard } from "../types"; -import { severityMap } from "./hui-gauge-card"; -import type { EnergyCarbonGaugeCardConfig } from "./types"; +} from "../../../../data/history"; +import type { HomeAssistant } from "../../../../types"; +import { createEntityNotFoundWarning } from "../../components/hui-warning"; +import type { LovelaceCard } from "../../types"; +import { severityMap } from "../hui-gauge-card"; +import type { EnergyCarbonGaugeCardConfig } from "../types"; @customElement("hui-energy-carbon-consumed-gauge-card") class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { @@ -103,7 +103,7 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { (totalSolarProduction || 0) - (totalGridReturned || 0); - value = round((highCarbonEnergy / totalEnergyConsumed) * 100); + value = round((1 - highCarbonEnergy / totalEnergyConsumed) * 100); } return html` @@ -116,23 +116,23 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { .locale=${this.hass!.locale} label="%" style=${styleMap({ - "--gauge-color": this._computeSeverity(64), + "--gauge-color": this._computeSeverity(value), })} > -
High-carbon energy consumed
` - : html`Consumed high-carbon energy couldn't be calculated`} +
Non-fossil energy consumed
` + : html`Consumed non-fossil energy couldn't be calculated`} `; } private _computeSeverity(numberValue: number): string { - if (numberValue > 50) { + if (numberValue < 10) { return severityMap.red; } - if (numberValue > 30) { + if (numberValue < 30) { return severityMap.yellow; } - if (numberValue < 10) { + if (numberValue > 75) { return severityMap.green; } return severityMap.normal; diff --git a/src/panels/lovelace/cards/hui-energy-costs-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-costs-table-card.ts similarity index 94% rename from src/panels/lovelace/cards/hui-energy-costs-table-card.ts rename to src/panels/lovelace/cards/energy/hui-energy-costs-table-card.ts index 9c3a843c0f..1a886d620b 100644 --- a/src/panels/lovelace/cards/hui-energy-costs-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-costs-table-card.ts @@ -9,23 +9,23 @@ import { unsafeCSS, } from "lit"; import { customElement, property, state } from "lit/decorators"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import { round } from "../../../common/number/round"; -import "../../../components/chart/statistics-chart"; -import "../../../components/ha-card"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import { round } from "../../../../common/number/round"; +import "../../../../components/chart/statistics-chart"; +import "../../../../components/ha-card"; import { EnergyInfo, getEnergyInfo, GridSourceTypeEnergyPreference, -} from "../../../data/energy"; +} from "../../../../data/energy"; import { calculateStatisticSumGrowth, fetchStatistics, Statistics, -} from "../../../data/history"; -import { HomeAssistant } from "../../../types"; -import { LovelaceCard } from "../types"; -import { EnergyDevicesGraphCardConfig } from "./types"; +} from "../../../../data/history"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergyDevicesGraphCardConfig } from "../types"; @customElement("hui-energy-costs-table-card") export class HuiEnergyCostsTableCard diff --git a/src/panels/lovelace/cards/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts similarity index 92% rename from src/panels/lovelace/cards/hui-energy-devices-graph-card.ts rename to src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 2897dc386b..66e4a75518 100644 --- a/src/panels/lovelace/cards/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -14,18 +14,18 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { getColorByIndex } from "../../../common/color/colors"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import "../../../components/chart/ha-chart-base"; -import "../../../components/ha-card"; +import { getColorByIndex } from "../../../../common/color/colors"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import "../../../../components/chart/ha-chart-base"; +import "../../../../components/ha-card"; import { calculateStatisticSumGrowth, fetchStatistics, Statistics, -} from "../../../data/history"; -import { HomeAssistant } from "../../../types"; -import { LovelaceCard } from "../types"; -import { EnergyDevicesGraphCardConfig } from "./types"; +} from "../../../../data/history"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergyDevicesGraphCardConfig } from "../types"; @customElement("hui-energy-devices-graph-card") export class HuiEnergyDevicesGraphCard diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts new file mode 100644 index 0000000000..a75ad559cf --- /dev/null +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -0,0 +1,505 @@ +import { + mdiArrowLeft, + mdiArrowRight, + mdiHome, + mdiLeaf, + mdiSolarPower, + mdiTransmissionTower, +} from "@mdi/js"; +import { css, html, LitElement, svg } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; +import { round } from "../../../../common/number/round"; +import { subscribeOne } from "../../../../common/util/subscribe-one"; +import "../../../../components/ha-card"; +import "../../../../components/ha-svg-icon"; +import { getConfigEntries } from "../../../../data/config_entries"; +import { energySourcesByType } from "../../../../data/energy"; +import { subscribeEntityRegistry } from "../../../../data/entity_registry"; +import { + calculateStatisticsSumGrowth, + fetchStatistics, + Statistics, +} from "../../../../data/history"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergyDistributionCardConfig } from "../types"; + +const CIRCLE_CIRCUMFERENCE = 238.76104; + +@customElement("hui-energy-distribution-card") +class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _config?: EnergyDistributionCardConfig; + + @state() private _stats?: Statistics; + + @state() private _co2SignalEntity?: string; + + private _fetching = false; + + public setConfig(config: EnergyDistributionCardConfig): void { + this._config = config; + } + + public getCardSize(): Promise | number { + return 3; + } + + public willUpdate(changedProps) { + super.willUpdate(changedProps); + + if (!this._fetching && !this._stats) { + this._fetching = true; + Promise.all([this._getStatistics(), this._fetchCO2SignalEntity()]).then( + () => { + this._fetching = false; + } + ); + } + } + + protected render() { + if (!this._config) { + return html``; + } + + if (!this._stats) { + return html`Loading…`; + } + + const prefs = this._config!.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 hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; + + const totalGridConsumption = + calculateStatisticsSumGrowth( + this._stats, + types.grid![0].flow_from.map((flow) => flow.stat_energy_from) + ) ?? 0; + + let totalSolarProduction: number | null = null; + + if (hasSolarProduction) { + totalSolarProduction = calculateStatisticsSumGrowth( + this._stats, + types.solar!.map((source) => source.stat_energy_from) + ); + } + + let productionReturnedToGrid: number | null = null; + + if (hasReturnToGrid) { + productionReturnedToGrid = calculateStatisticsSumGrowth( + this._stats, + types.grid![0].flow_to.map((flow) => flow.stat_energy_to) + ); + } + + // total consumption = consumption_from_grid + solar_production - return_to_grid + + let co2percentage: number | undefined; + + if (this._co2SignalEntity) { + const co2State = this.hass.states[this._co2SignalEntity]; + if (co2State) { + co2percentage = Number(co2State.state); + if (isNaN(co2percentage)) { + co2percentage = undefined; + } + } + } + + const totalConsumption = + totalGridConsumption + + (totalSolarProduction || 0) - + (productionReturnedToGrid || 0); + + let homeSolarCircumference: number | undefined; + if (hasSolarProduction) { + const homePctSolar = + ((totalSolarProduction || 0) - (productionReturnedToGrid || 0)) / + totalConsumption; + homeSolarCircumference = CIRCLE_CIRCUMFERENCE * homePctSolar; + } + + let lowCarbonConsumption: number | undefined; + + let homeLowCarbonCircumference: number | undefined; + let homeHighCarbonCircumference: number | undefined; + if (co2percentage !== undefined) { + const gridPctHighCarbon = co2percentage / 100; + + lowCarbonConsumption = + totalGridConsumption - totalGridConsumption * gridPctHighCarbon; + + const homePctGridHighCarbon = + (gridPctHighCarbon * totalGridConsumption) / totalConsumption; + + homeHighCarbonCircumference = + CIRCLE_CIRCUMFERENCE * homePctGridHighCarbon; + + homeLowCarbonCircumference = + CIRCLE_CIRCUMFERENCE - + (homeSolarCircumference || 0) - + homeHighCarbonCircumference; + } + + return html` + +
+ ${lowCarbonConsumption !== undefined || hasSolarProduction + ? html`
+ ${lowCarbonConsumption === undefined + ? html`
` + : html` +
+ Non-fossil +
+ + ${round(lowCarbonConsumption, 1)} kWh +
+ + + +
+ `} + ${hasSolarProduction + ? html`
+ Solar +
+ + ${round(totalSolarProduction || 0, 1)} kWh +
+
` + : ""} +
+
` + : ""} +
+
+
+ + + ${hasReturnToGrid + ? html`` + : ""}${round(totalGridConsumption, 1)} + kWh + + ${productionReturnedToGrid + ? html` + ${round(productionReturnedToGrid, 1)} kWh + ` + : ""} +
+ Grid +
+
+
+ + ${round(totalConsumption, 1)} kWh + ${homeSolarCircumference !== undefined || + homeLowCarbonCircumference !== undefined + ? html` + ${homeSolarCircumference !== undefined + ? svg` + ` + : ""} + ${homeHighCarbonCircumference + ? svg` + ` + : ""} + + ` + : ""} +
+ Home +
+
+
+ + ${productionReturnedToGrid && hasSolarProduction + ? svg`` + : ""} + ${totalSolarProduction + ? svg`` + : ""} + ${totalGridConsumption + ? svg`` + : ""} + +
+
+
+ `; + } + + private async _fetchCO2SignalEntity() { + const [configEntries, entityRegistryEntries] = await Promise.all([ + getConfigEntries(this.hass), + subscribeOne(this.hass.connection, subscribeEntityRegistry), + ]); + + const co2ConfigEntry = configEntries.find( + (entry) => entry.domain === "co2signal" + ); + + if (!co2ConfigEntry) { + return; + } + + for (const entry of entityRegistryEntries) { + if (entry.config_entry_id !== co2ConfigEntry.entry_id) { + continue; + } + + // The integration offers 2 entities. We want the % one. + const co2State = this.hass.states[entry.entity_id]; + if (!co2State || co2State.attributes.unit_of_measurement !== "%") { + continue; + } + + this._co2SignalEntity = co2State.entity_id; + break; + } + } + + 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") { + statistics.push(source.stat_energy_from); + 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 styles = css` + :host { + --mdc-icon-size: 24px; + } + .card-content { + position: relative; + } + .lines { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 146px; + display: flex; + justify-content: center; + padding: 0 16px 16px; + box-sizing: border-box; + } + .lines svg { + width: calc(100% - 160px); + height: 100%; + max-width: 340px; + } + .row { + display: flex; + justify-content: space-between; + max-width: 500px; + margin: 0 auto; + } + .circle-container { + display: flex; + flex-direction: column; + align-items: center; + } + .circle-container.solar { + height: 130px; + } + .spacer { + width: 80px; + height: 30px; + } + .circle { + width: 80px; + height: 80px; + border-radius: 50%; + box-sizing: border-box; + border: 2px solid; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + font-size: 12px; + line-height: 12px; + position: relative; + } + ha-svg-icon { + padding-bottom: 2px; + } + ha-svg-icon.small { + --mdc-icon-size: 12px; + } + .label { + color: var(--secondary-text-color); + font-size: 12px; + } + line, + path { + stroke: var(--primary-text-color); + stroke-width: 1; + fill: none; + } + .circle svg { + position: absolute; + fill: none; + stroke-width: 4px; + width: 100%; + height: 100%; + } + .circle svg circle { + animation: rotate-in 0.2s ease-in; + } + .low-carbon line { + stroke: #0f9d58; + } + .low-carbon .circle { + border-color: #0f9d58; + } + .low-carbon ha-svg-icon { + color: #0f9d58; + } + .solar .circle { + border-color: #ff9800; + } + path.solar, + circle.solar { + stroke: #ff9800; + } + circle.low-carbon { + stroke: #0f9d58; + } + circle.return, + path.return { + stroke: #673ab7; + } + .return { + color: #673ab7; + } + .grid .circle { + border-color: #126a9a; + } + .consumption { + color: #126a9a; + } + circle.grid, + path.grid { + stroke: #126a9a; + } + .home .circle { + border: none; + } + .home .circle.border { + border-color: var(--primary-color); + } + @keyframes rotate-in { + from { + stroke-dashoffset: 0; + } + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-energy-distribution-card": HuiEnergyDistrubutionCard; + } +} diff --git a/src/panels/lovelace/cards/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts similarity index 86% rename from src/panels/lovelace/cards/hui-energy-solar-consumed-gauge-card.ts rename to src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index be3ad03e03..f6582c9af6 100644 --- a/src/panels/lovelace/cards/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -1,19 +1,19 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; -import { round } from "../../../common/number/round"; -import "../../../components/ha-card"; -import "../../../components/ha-gauge"; -import { energySourcesByType } from "../../../data/energy"; +import { round } from "../../../../common/number/round"; +import "../../../../components/ha-card"; +import "../../../../components/ha-gauge"; +import { energySourcesByType } from "../../../../data/energy"; import { calculateStatisticsSumGrowth, fetchStatistics, Statistics, -} from "../../../data/history"; -import type { HomeAssistant } from "../../../types"; -import type { LovelaceCard } from "../types"; -import { severityMap } from "./hui-gauge-card"; -import type { EnergySolarGaugeCardConfig } from "./types"; +} from "../../../../data/history"; +import type { HomeAssistant } from "../../../../types"; +import type { LovelaceCard } from "../../types"; +import { severityMap } from "../hui-gauge-card"; +import type { EnergySolarGaugeCardConfig } from "../types"; @customElement("hui-energy-solar-consumed-gauge-card") class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { @@ -77,7 +77,7 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { .locale=${this.hass!.locale} label="%" style=${styleMap({ - "--gauge-color": this._computeSeverity(64), + "--gauge-color": this._computeSeverity(value), })} >
Self consumed solar energy
` @@ -87,9 +87,12 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { } private _computeSeverity(numberValue: number): string { - if (numberValue > 50) { + if (numberValue > 75) { return severityMap.green; } + if (numberValue < 50) { + return severityMap.yellow; + } return severityMap.normal; } diff --git a/src/panels/lovelace/cards/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts similarity index 85% rename from src/panels/lovelace/cards/hui-energy-solar-graph-card.ts rename to src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index c258ac3f79..6e3c756e47 100644 --- a/src/panels/lovelace/cards/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -8,31 +8,31 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import "../../../components/ha-card"; +import "../../../../components/ha-card"; import { ChartData, ChartDataset, ChartOptions } from "chart.js"; -import { HomeAssistant } from "../../../types"; -import { LovelaceCard } from "../types"; -import { EnergySolarGraphCardConfig } from "./types"; -import { fetchStatistics, Statistics } from "../../../data/history"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergySolarGraphCardConfig } from "../types"; +import { fetchStatistics, Statistics } from "../../../../data/history"; import { hex2rgb, lab2rgb, rgb2hex, rgb2lab, -} from "../../../common/color/convert-color"; -import { labDarken } from "../../../common/color/lab"; -import { SolarSourceTypeEnergyPreference } from "../../../data/energy"; -import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +} from "../../../../common/color/convert-color"; +import { labDarken } from "../../../../common/color/lab"; +import { SolarSourceTypeEnergyPreference } from "../../../../data/energy"; +import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { ForecastSolarForecast, getForecastSolarForecasts, -} from "../../../data/forecast_solar"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import "../../../components/chart/ha-chart-base"; -import "../../../components/ha-switch"; -import "../../../components/ha-formfield"; +} from "../../../../data/forecast_solar"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import "../../../../components/chart/ha-chart-base"; +import "../../../../components/ha-switch"; +import "../../../../components/ha-formfield"; -const SOLAR_COLOR = { border: "#FF9800", background: "#ffcb80" }; +const SOLAR_COLOR = "#FF9800"; @customElement("hui-energy-solar-graph-card") export class HuiEnergySolarGraphCard @@ -123,17 +123,11 @@ export class HuiEnergySolarGraphCard "has-header": !!this._config.title, })}" > - ${this._chartData ? html`` : ""} @@ -142,12 +136,18 @@ export class HuiEnergySolarGraphCard } private _createOptions() { + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + const startTime = startDate.getTime(); + this._chartOptions = { parsing: false, animation: false, scales: { x: { type: "time", + suggestedMin: startTime, + suggestedMax: startTime + 24 * 60 * 60 * 1000, adapters: { date: { locale: this.hass.locale, @@ -168,9 +168,14 @@ export class HuiEnergySolarGraphCard time: { tooltipFormat: "datetimeseconds", }, + offset: true, }, y: { type: "linear", + title: { + display: true, + text: "kWh", + }, ticks: { beginAtZero: true, }, @@ -199,9 +204,10 @@ export class HuiEnergySolarGraphCard }, elements: { line: { - tension: 0.4, + tension: 0.3, borderWidth: 1.5, }, + bar: { borderWidth: 1.5 }, point: { hitRadius: 5, }, @@ -252,7 +258,7 @@ export class HuiEnergySolarGraphCard ) as SolarSourceTypeEnergyPreference[]; const statisticsData = Object.values(this._data!); - const datasets: ChartDataset<"line">[] = []; + const datasets: ChartDataset<"bar">[] = []; let endTime: Date; if (statisticsData.length === 0) { @@ -272,22 +278,18 @@ export class HuiEnergySolarGraphCard } solarSources.forEach((source, idx) => { - const data: ChartDataset<"line">[] = []; + const data: ChartDataset<"bar" | "line">[] = []; const entity = this.hass.states[source.stat_energy_from]; const borderColor = idx > 0 - ? rgb2hex( - lab2rgb(labDarken(rgb2lab(hex2rgb(SOLAR_COLOR.border)), idx)) - ) - : SOLAR_COLOR.border; + ? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(SOLAR_COLOR)), idx))) + : SOLAR_COLOR; data.push({ label: `Production ${ entity ? computeStateName(entity) : source.stat_energy_from }`, - fill: true, - stepped: false, borderColor: borderColor, backgroundColor: borderColor + "7F", data: [], @@ -343,6 +345,7 @@ export class HuiEnergySolarGraphCard if (forecastsData) { const forecast: ChartDataset<"line"> = { + type: "line", label: `Forecast ${ entity ? computeStateName(entity) : source.stat_energy_from }`, @@ -382,11 +385,6 @@ export class HuiEnergySolarGraphCard }; } - private _showAllForecastChanged(ev) { - this._showAllForecastData = ev.target.checked; - this._renderChart(); - } - static get styles(): CSSResultGroup { return css` ha-card { diff --git a/src/panels/lovelace/cards/hui-energy-summary-card.ts b/src/panels/lovelace/cards/energy/hui-energy-summary-card.ts similarity index 96% rename from src/panels/lovelace/cards/hui-energy-summary-card.ts rename to src/panels/lovelace/cards/energy/hui-energy-summary-card.ts index 0b5c47ccb3..9fd7dd20e2 100644 --- a/src/panels/lovelace/cards/hui-energy-summary-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-summary-card.ts @@ -1,21 +1,21 @@ import { mdiCashMultiple, mdiSolarPower } from "@mdi/js"; import { css, html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; -import "../../../components/ha-svg-icon"; +import "../../../../components/ha-svg-icon"; import { energySourcesByType, GridSourceTypeEnergyPreference, SolarSourceTypeEnergyPreference, -} from "../../../data/energy"; +} from "../../../../data/energy"; import { calculateStatisticSumGrowth, fetchStatistics, Statistics, -} from "../../../data/history"; -import { HomeAssistant } from "../../../types"; -import { LovelaceCard } from "../types"; -import { EnergySummaryCardConfig } from "./types"; -import "../../../components/ha-card"; +} from "../../../../data/history"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergySummaryCardConfig } from "../types"; +import "../../../../components/ha-card"; const renderSumStatHelper = ( data: Statistics, diff --git a/src/panels/lovelace/cards/hui-energy-summary-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-summary-graph-card.ts similarity index 86% rename from src/panels/lovelace/cards/hui-energy-summary-graph-card.ts rename to src/panels/lovelace/cards/energy/hui-energy-summary-graph-card.ts index f0ceab258e..7e98facdac 100644 --- a/src/panels/lovelace/cards/hui-energy-summary-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-summary-graph-card.ts @@ -8,33 +8,28 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import "../../../components/ha-card"; +import "../../../../components/ha-card"; import { ChartData, ChartDataset, ChartOptions } from "chart.js"; -import { HomeAssistant } from "../../../types"; -import { LovelaceCard } from "../types"; -import { EnergySummaryGraphCardConfig } from "./types"; -import { fetchStatistics, Statistics } from "../../../data/history"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergySummaryGraphCardConfig } from "../types"; +import { fetchStatistics, Statistics } from "../../../../data/history"; import { hex2rgb, lab2rgb, rgb2hex, rgb2lab, -} from "../../../common/color/convert-color"; -import { labDarken } from "../../../common/color/lab"; -import { computeStateName } from "../../../common/entity/compute_state_name"; -import "../../../components/chart/ha-chart-base"; -import { round } from "../../../common/number/round"; +} from "../../../../common/color/convert-color"; +import { labDarken } from "../../../../common/color/lab"; +import { computeStateName } from "../../../../common/entity/compute_state_name"; +import "../../../../components/chart/ha-chart-base"; +import { round } from "../../../../common/number/round"; const NEGATIVE = ["to_grid"]; -const ORDER = { - used_solar: 0, - from_grid: 100, - to_grid: 200, -}; const COLORS = { - to_grid: { border: "#56d256", background: "#87ceab" }, - from_grid: { border: "#126A9A", background: "#88b5cd" }, - used_solar: { border: "#FF9800", background: "#ffcb80" }, + to_grid: { border: "#673ab7", background: "#b39bdb" }, + from_grid: { border: "#126A9A", background: "#8ab5cd" }, + used_solar: { border: "#FF9800", background: "#fecc8e" }, }; @customElement("hui-energy-summary-graph-card") @@ -126,7 +121,7 @@ export class HuiEnergySummaryGraphCard ? html`` : ""} @@ -135,12 +130,18 @@ export class HuiEnergySummaryGraphCard } private _createOptions() { + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + const startTime = startDate.getTime(); + this._chartOptions = { parsing: false, animation: false, scales: { x: { type: "time", + suggestedMin: startTime, + suggestedMax: startTime + 24 * 60 * 60 * 1000, adapters: { date: { locale: this.hass.locale, @@ -161,10 +162,15 @@ export class HuiEnergySummaryGraphCard time: { tooltipFormat: "datetimeseconds", }, + offset: true, }, y: { stacked: true, type: "linear", + title: { + display: true, + text: "kWh", + }, ticks: { beginAtZero: true, callback: (value) => Math.abs(round(value)), @@ -193,9 +199,13 @@ export class HuiEnergySummaryGraphCard } } return [ - `Total consumed: ${totalConsumed.toFixed(2)} kWh`, - `Total returned: ${totalReturned.toFixed(2)} kWh`, - ]; + totalConsumed + ? `Total consumed: ${totalConsumed.toFixed(2)} kWh` + : "", + totalReturned + ? `Total returned: ${totalReturned.toFixed(2)} kWh` + : "", + ].filter(Boolean); }, }, }, @@ -213,10 +223,7 @@ export class HuiEnergySummaryGraphCard mode: "nearest", }, elements: { - line: { - tension: 0.4, - borderWidth: 1.5, - }, + bar: { borderWidth: 1.5 }, point: { hitRadius: 5, }, @@ -280,7 +287,7 @@ export class HuiEnergySummaryGraphCard } const statisticsData = Object.values(this._data!); - const datasets: ChartDataset<"line">[] = []; + const datasets: ChartDataset<"bar">[] = []; let endTime: Date; if (statisticsData.length === 0) { @@ -370,7 +377,7 @@ export class HuiEnergySummaryGraphCard const negative = NEGATIVE.includes(type); Object.entries(sources).forEach(([statId, source], idx) => { - const data: ChartDataset<"line">[] = []; + const data: ChartDataset<"bar">[] = []; const entity = this.hass.states[statId]; const color = COLORS[type]; @@ -381,9 +388,6 @@ export class HuiEnergySummaryGraphCard : entity ? computeStateName(entity) : statId, - fill: true, - stepped: false, - order: ORDER[type] + idx, borderColor: idx > 0 ? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(color.border)), idx))) @@ -394,7 +398,7 @@ export class HuiEnergySummaryGraphCard lab2rgb(labDarken(rgb2lab(hex2rgb(color.background)), idx)) ) : color.background, - stack: negative ? "negative" : "positive", + stack: "stack", data: [], }); @@ -402,6 +406,7 @@ export class HuiEnergySummaryGraphCard for (const key of uniqueKeys) { const value = key in source ? Math.round(source[key] * 100) / 100 : 0; const date = new Date(key); + // @ts-expect-error data[0].data.push({ x: date.getTime(), y: value && negative ? -1 * value : value, diff --git a/src/panels/lovelace/cards/hui-energy-usage-card.ts b/src/panels/lovelace/cards/hui-energy-usage-card.ts deleted file mode 100644 index f297fbdaf3..0000000000 --- a/src/panels/lovelace/cards/hui-energy-usage-card.ts +++ /dev/null @@ -1,322 +0,0 @@ -import { mdiHome, mdiLeaf, mdiSolarPower, mdiTransmissionTower } from "@mdi/js"; -import { css, html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { subscribeOne } from "../../../common/util/subscribe-one"; -import "../../../components/ha-svg-icon"; -import { getConfigEntries } from "../../../data/config_entries"; -import { energySourcesByType } from "../../../data/energy"; -import { subscribeEntityRegistry } from "../../../data/entity_registry"; -import { - calculateStatisticsSumGrowth, - fetchStatistics, - Statistics, -} from "../../../data/history"; -import { HomeAssistant } from "../../../types"; -import { LovelaceCard } from "../types"; -import { EnergySummaryCardConfig } from "./types"; -import "../../../components/ha-card"; -import { round } from "../../../common/number/round"; - -@customElement("hui-energy-usage-card") -class HuiEnergyUsageCard extends LitElement implements LovelaceCard { - @property({ attribute: false }) public hass!: HomeAssistant; - - @state() private _config?: EnergySummaryCardConfig; - - @state() private _stats?: Statistics; - - @state() private _co2SignalEntity?: string; - - private _fetching = false; - - public setConfig(config: EnergySummaryCardConfig): void { - this._config = config; - } - - public getCardSize(): Promise | number { - return 3; - } - - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this._fetching && !this._stats) { - this._fetching = true; - Promise.all([this._getStatistics(), this._fetchCO2SignalEntity()]).then( - () => { - this._fetching = false; - } - ); - } - } - - protected render() { - if (!this._config) { - return html``; - } - - if (!this._stats) { - return html`Loading…`; - } - - const prefs = this._config!.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 hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0; - - const totalGridConsumption = - calculateStatisticsSumGrowth( - this._stats, - types.grid![0].flow_from.map((flow) => flow.stat_energy_from) - ) ?? 0; - - let totalSolarProduction: number | null = null; - - if (hasSolarProduction) { - totalSolarProduction = calculateStatisticsSumGrowth( - this._stats, - types.solar!.map((source) => source.stat_energy_from) - ); - } - - let productionReturnedToGrid: number | null = null; - - if (hasReturnToGrid) { - productionReturnedToGrid = calculateStatisticsSumGrowth( - this._stats, - types.grid![0].flow_to.map((flow) => flow.stat_energy_to) - ); - } - - // total consumption = consumption_from_grid + solar_production - return_to_grid - - let co2percentage: number | undefined; - - if (this._co2SignalEntity) { - const co2State = this.hass.states[this._co2SignalEntity]; - if (co2State) { - co2percentage = Number(co2State.state); - if (isNaN(co2percentage)) { - co2percentage = undefined; - } - } - } - - // We are calculating low carbon consumption based on what we got from the grid - // minus what we gave back because what we gave back is low carbon - const relativeGridFlow = - totalGridConsumption - (productionReturnedToGrid || 0); - - let lowCarbonConsumption: number | undefined; - - if (co2percentage !== undefined) { - if (relativeGridFlow > 0) { - lowCarbonConsumption = round(relativeGridFlow * (co2percentage / 100)); - } else { - lowCarbonConsumption = 0; - } - } - - const totalConsumption = - totalGridConsumption + - (totalSolarProduction || 0) - - (productionReturnedToGrid || 0); - - const gridPctLowCarbon = - co2percentage === undefined ? 0 : co2percentage / 100; - const gridPctHighCarbon = 1 - gridPctLowCarbon; - - const homePctSolar = - ((totalSolarProduction || 0) - (productionReturnedToGrid || 0)) / - totalConsumption; - // When we know the ratio solar-grid, we can adjust the low/high carbon - // percentages to reflect that. - const homePctGridLowCarbon = gridPctLowCarbon * (1 - homePctSolar); - const homePctGridHighCarbon = gridPctHighCarbon * (1 - homePctSolar); - - return html` - -
-
- ${co2percentage === undefined - ? "" - : html` -
- Low-carbon -
- - ${co2percentage}% / ${round(lowCarbonConsumption!)} kWh -
-
- `} -
- Solar -
- - ${round(totalSolarProduction || 0)} kWh -
-
-
-
-
-
- - ${round(totalGridConsumption - (productionReturnedToGrid || 0))} - kWh -
    -
  • - Grid high carbon: ${round(gridPctHighCarbon * 100, 1)}% -
  • -
  • Grid low carbon: ${round(gridPctLowCarbon * 100, 1)}%
  • -
-
- Grid -
-
-
- - ${round(totalConsumption)} kWh -
    -
  • - Grid high carbon: ${round(homePctGridHighCarbon * 100)}% -
  • -
  • - Grid low carbon: ${round(homePctGridLowCarbon * 100)}% -
  • -
  • Solar: ${round(homePctSolar * 100)}%
  • -
-
- Home -
-
-
-
- `; - } - - private async _fetchCO2SignalEntity() { - const [configEntries, entityRegistryEntries] = await Promise.all([ - getConfigEntries(this.hass), - subscribeOne(this.hass.connection, subscribeEntityRegistry), - ]); - - const co2ConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); - - if (!co2ConfigEntry) { - return; - } - - for (const entry of entityRegistryEntries) { - if (entry.config_entry_id !== co2ConfigEntry.entry_id) { - continue; - } - - // The integration offers 2 entities. We want the % one. - const co2State = this.hass.states[entry.entity_id]; - if (!co2State || co2State.attributes.unit_of_measurement !== "%") { - continue; - } - - this._co2SignalEntity = co2State.entity_id; - break; - } - } - - 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") { - statistics.push(source.stat_energy_from); - 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 styles = css` - :host { - --mdc-icon-size: 26px; - } - .row { - display: flex; - margin-bottom: 30px; - } - .row:last-child { - margin-bottom: 0; - } - .circle-container { - display: flex; - flex-direction: column; - align-items: center; - margin-right: 40px; - } - .circle { - width: 80px; - height: 80px; - border-radius: 50%; - border: 2px solid; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - font-size: 12px; - } - .label { - color: var(--secondary-text-color); - font-size: 12px; - } - .circle-container:last-child { - margin-right: 0; - } - .circle ul { - display: none; - } - .low-carbon { - border-color: #0da035; - } - .low-carbon ha-svg-icon { - color: #0da035; - } - .solar { - border-color: #ff9800; - } - .grid { - border-color: #134763; - } - .circle-container.home { - margin-left: 120px; - } - `; -} - -declare global { - interface HTMLElementTagNameMap { - "hui-energy-usage-card": HuiEnergyUsageCard; - } -} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index ebaecf3e9e..dcbf69054a 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -92,31 +92,42 @@ export interface ButtonCardConfig extends LovelaceCardConfig { export interface EnergySummaryCardConfig extends LovelaceCardConfig { type: "energy-summary"; + title?: string; prefs: EnergyPreferences; } +export interface EnergyDistributionCardConfig extends LovelaceCardConfig { + type: "energy-distribution"; + title?: string; + prefs: EnergyPreferences; +} export interface EnergySummaryGraphCardConfig extends LovelaceCardConfig { type: "energy-summary-graph"; + title?: string; prefs: EnergyPreferences; } export interface EnergySolarGraphCardConfig extends LovelaceCardConfig { type: "energy-solar-graph"; + title?: string; prefs: EnergyPreferences; } export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig { type: "energy-devices-graph"; + title?: string; prefs: EnergyPreferences; } export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { type: "energy-solar-consumed-gauge"; + title?: string; prefs: EnergyPreferences; } export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig { type: "energy-carbon-consumed-gauge"; + title?: string; prefs: EnergyPreferences; } diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 33871cfe40..0524406b79 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -35,18 +35,21 @@ const LAZY_LOAD_TYPES = { "alarm-panel": () => import("../cards/hui-alarm-panel-card"), error: () => import("../cards/hui-error-card"), "empty-state": () => import("../cards/hui-empty-state-card"), - "energy-summary": () => import("../cards/hui-energy-summary-card"), + "energy-summary": () => import("../cards/energy/hui-energy-summary-card"), "energy-summary-graph": () => - import("../cards/hui-energy-summary-graph-card"), - "energy-solar-graph": () => import("../cards/hui-energy-solar-graph-card"), + import("../cards/energy/hui-energy-summary-graph-card"), + "energy-solar-graph": () => + import("../cards/energy/hui-energy-solar-graph-card"), "energy-devices-graph": () => - import("../cards/hui-energy-devices-graph-card"), - "energy-costs-table": () => import("../cards/hui-energy-costs-table-card"), - "energy-usage": () => import("../cards/hui-energy-usage-card"), + import("../cards/energy/hui-energy-devices-graph-card"), + "energy-costs-table": () => + import("../cards/energy/hui-energy-costs-table-card"), + "energy-distribution": () => + import("../cards/energy/hui-energy-distribution-card"), "energy-solar-consumed-gauge": () => - import("../cards/hui-energy-solar-consumed-gauge-card"), + import("../cards/energy/hui-energy-solar-consumed-gauge-card"), "energy-carbon-consumed-gauge": () => - import("../cards/hui-energy-carbon-consumed-gauge-card"), + import("../cards/energy/hui-energy-carbon-consumed-gauge-card"), grid: () => import("../cards/hui-grid-card"), starting: () => import("../cards/hui-starting-card"), "entity-filter": () => import("../cards/hui-entity-filter-card"),