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``
+ : ""}
+
+
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: 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"),