- `${context.dataset.label}: ${context.parsed.y} kWh`,
+ `${context.dataset.label}: ${formatNumber(
+ context.parsed.y,
+ this.hass.locale
+ )} kWh`,
},
},
filler: {
@@ -212,6 +220,8 @@ export class HuiEnergySolarGraphCard
hitRadius: 5,
},
},
+ // @ts-expect-error
+ locale: numberFormatToLocale(this.hass.locale),
};
}
@@ -219,6 +229,7 @@ export class HuiEnergySolarGraphCard
if (this._fetching) {
return;
}
+
const startDate = new Date();
startDate.setHours(0, 0, 0, 0);
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
@@ -273,20 +284,25 @@ export class HuiEnergySolarGraphCard
endTime = new Date();
}
+ const computedStyles = getComputedStyle(this);
+ const solarColor = computedStyles
+ .getPropertyValue("--energy-solar-color")
+ .trim();
+
solarSources.forEach((source, idx) => {
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)), idx)))
- : SOLAR_COLOR;
+ ? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx)))
+ : solarColor;
data.push({
label: `Production ${
entity ? computeStateName(entity) : source.stat_energy_from
}`,
- borderColor: borderColor,
+ borderColor,
backgroundColor: borderColor + "7F",
data: [],
});
@@ -307,7 +323,7 @@ export class HuiEnergySolarGraphCard
if (prevStart === point.start) {
continue;
}
- const value = Math.round((point.sum - prevValue) * 100) / 100;
+ const value = point.sum - prevValue;
const date = new Date(point.start);
data[0].data.push({
x: date.getTime(),
@@ -347,7 +363,9 @@ export class HuiEnergySolarGraphCard
}`,
fill: false,
stepped: false,
- borderColor: "#000",
+ borderColor: computedStyles.getPropertyValue(
+ "--primary-text-color"
+ ),
borderDash: [7, 5],
pointRadius: 0,
data: [],
@@ -386,6 +404,9 @@ export class HuiEnergySolarGraphCard
ha-card {
height: 100%;
}
+ .card-header {
+ padding-bottom: 0;
+ }
.content {
padding: 16px;
}
diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts
new file mode 100644
index 0000000000..44f4336609
--- /dev/null
+++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts
@@ -0,0 +1,426 @@
+// @ts-ignore
+import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css";
+import {
+ css,
+ CSSResultGroup,
+ html,
+ LitElement,
+ TemplateResult,
+ unsafeCSS,
+} from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { styleMap } from "lit/directives/style-map";
+import {
+ rgb2hex,
+ lab2rgb,
+ rgb2lab,
+ hex2rgb,
+} from "../../../../common/color/convert-color";
+import { labDarken } from "../../../../common/color/lab";
+import { computeStateName } from "../../../../common/entity/compute_state_name";
+import { formatNumber } from "../../../../common/string/format_number";
+import "../../../../components/chart/statistics-chart";
+import "../../../../components/ha-card";
+import {
+ EnergyInfo,
+ energySourcesByType,
+ getEnergyInfo,
+} from "../../../../data/energy";
+import {
+ calculateStatisticSumGrowth,
+ fetchStatistics,
+ Statistics,
+} from "../../../../data/history";
+import { HomeAssistant } from "../../../../types";
+import { LovelaceCard } from "../../types";
+import { EnergySourcesTableCardConfig } from "../types";
+
+@customElement("hui-energy-sources-table-card")
+export class HuiEnergySourcesTableCard
+ extends LitElement
+ implements LovelaceCard
+{
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @state() private _config?: EnergySourcesTableCardConfig;
+
+ @state() private _stats?: Statistics;
+
+ @state() private _energyInfo?: EnergyInfo;
+
+ public getCardSize(): Promise
| number {
+ return 3;
+ }
+
+ public setConfig(config: EnergySourcesTableCardConfig): void {
+ this._config = config;
+ }
+
+ public willUpdate() {
+ if (!this.hasUpdated) {
+ this._getEnergyInfo().then(() => this._getStatistics());
+ }
+ }
+
+ protected render(): TemplateResult {
+ if (!this.hass || !this._config) {
+ return html``;
+ }
+
+ if (!this._stats) {
+ return html`Loading...`;
+ }
+
+ let totalGrid = 0;
+ let totalSolar = 0;
+ let totalCost = 0;
+
+ const types = energySourcesByType(this._config.prefs);
+
+ const computedStyles = getComputedStyle(this);
+ const solarColor = computedStyles
+ .getPropertyValue("--energy-solar-color")
+ .trim();
+ const returnColor = computedStyles
+ .getPropertyValue("--energy-grid-return-color")
+ .trim();
+ const consumptionColor = computedStyles
+ .getPropertyValue("--energy-grid-consumption-color")
+ .trim();
+
+ const showCosts =
+ types.grid?.[0].flow_from.some(
+ (flow) =>
+ flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
+ ) ||
+ types.grid?.[0].flow_to.some(
+ (flow) =>
+ flow.stat_compensation ||
+ flow.entity_energy_price ||
+ flow.number_energy_price
+ );
+
+ return html`
+ ${this._config.title
+ ? html``
+ : ""}
+
+
+
+
+
+
+
+ ${types.solar?.map((source, idx) => {
+ const entity = this.hass.states[source.stat_energy_from];
+ const energy =
+ calculateStatisticSumGrowth(
+ this._stats![source.stat_energy_from]
+ ) || 0;
+ totalSolar += energy;
+ const color =
+ idx > 0
+ ? rgb2hex(
+ lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx))
+ )
+ : solarColor;
+ return html`
+
+
+ |
+
+ ${entity
+ ? computeStateName(entity)
+ : source.stat_energy_from}
+ |
+
+ ${formatNumber(energy, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html` | `
+ : ""}
+
`;
+ })}
+ ${types.solar
+ ? html`
+ |
+
+ Solar total
+ |
+
+ ${formatNumber(totalSolar, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html` | `
+ : ""}
+
`
+ : ""}
+ ${types.grid?.map(
+ (source) => html`${source.flow_from.map((flow, idx) => {
+ const entity = this.hass.states[flow.stat_energy_from];
+ const energy =
+ calculateStatisticSumGrowth(
+ this._stats![flow.stat_energy_from]
+ ) || 0;
+ totalGrid += energy;
+ const cost_stat =
+ flow.stat_cost ||
+ this._energyInfo!.cost_sensors[flow.stat_energy_from];
+ const cost = cost_stat
+ ? calculateStatisticSumGrowth(this._stats![cost_stat])
+ : null;
+ if (cost !== null) {
+ totalCost += cost;
+ }
+ const color =
+ idx > 0
+ ? rgb2hex(
+ lab2rgb(
+ labDarken(rgb2lab(hex2rgb(consumptionColor)), idx)
+ )
+ )
+ : consumptionColor;
+ return html`
+
+
+ |
+
+ ${entity
+ ? computeStateName(entity)
+ : flow.stat_energy_from}
+ |
+
+ ${formatNumber(energy, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html`
+ ${cost !== null
+ ? formatNumber(cost, this.hass.locale, {
+ style: "currency",
+ currency: this.hass.config.currency!,
+ })
+ : ""}
+ | `
+ : ""}
+
`;
+ })}
+ ${source.flow_to.map((flow, idx) => {
+ const entity = this.hass.states[flow.stat_energy_to];
+ const energy =
+ (calculateStatisticSumGrowth(
+ this._stats![flow.stat_energy_to]
+ ) || 0) * -1;
+ totalGrid += energy;
+ const cost_stat =
+ flow.stat_compensation ||
+ this._energyInfo!.cost_sensors[flow.stat_energy_to];
+ const cost = cost_stat
+ ? calculateStatisticSumGrowth(this._stats![cost_stat])
+ : null;
+ if (cost !== null) {
+ totalCost += cost;
+ }
+ const color =
+ idx > 0
+ ? rgb2hex(
+ lab2rgb(labDarken(rgb2lab(hex2rgb(returnColor)), idx))
+ )
+ : returnColor;
+ return html`
+
+
+ |
+
+ ${entity ? computeStateName(entity) : flow.stat_energy_to}
+ |
+
+ ${formatNumber(energy, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html`
+ ${cost !== null
+ ? formatNumber(cost, this.hass.locale, {
+ style: "currency",
+ currency: this.hass.config.currency!,
+ })
+ : ""}
+ | `
+ : ""}
+
`;
+ })}`
+ )}
+ ${types.grid
+ ? html`
+ |
+ Grid total |
+
+ ${formatNumber(totalGrid, this.hass.locale)} kWh
+ |
+ ${showCosts
+ ? html`
+ ${formatNumber(totalCost, this.hass.locale, {
+ style: "currency",
+ currency: this.hass.config.currency!,
+ })}
+ | `
+ : ""}
+
`
+ : ""}
+
+
+
+
+ `;
+ }
+
+ private async _getEnergyInfo() {
+ this._energyInfo = await getEnergyInfo(this.hass);
+ }
+
+ 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[] = Object.values(this._energyInfo!.cost_sensors);
+ const prefs = this._config!.prefs;
+ for (const source of prefs.energy_sources) {
+ if (source.type === "solar") {
+ statistics.push(source.stat_energy_from);
+ } else {
+ // grid source
+ for (const flowFrom of source.flow_from) {
+ statistics.push(flowFrom.stat_energy_from);
+ if (flowFrom.stat_cost) {
+ statistics.push(flowFrom.stat_cost);
+ }
+ }
+ for (const flowTo of source.flow_to) {
+ statistics.push(flowTo.stat_energy_to);
+ if (flowTo.stat_compensation) {
+ statistics.push(flowTo.stat_compensation);
+ }
+ }
+ }
+ }
+
+ this._stats = await fetchStatistics(
+ this.hass!,
+ startDate,
+ undefined,
+ statistics
+ );
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ${unsafeCSS(dataTableStyles)}
+ .mdc-data-table {
+ width: 100%;
+ border: 0;
+ }
+ .mdc-data-table__header-cell,
+ .mdc-data-table__cell {
+ color: var(--primary-text-color);
+ border-bottom-color: var(--divider-color);
+ }
+ .mdc-data-table__row:not(.mdc-data-table__row--selected):hover {
+ background-color: rgba(var(--rgb-primary-text-color), 0.04);
+ }
+ .total {
+ --mdc-typography-body2-font-weight: 500;
+ }
+ .total .mdc-data-table__cell {
+ border-top: 1px solid var(--divider-color);
+ }
+ ha-card {
+ height: 100%;
+ }
+ .card-header {
+ padding-bottom: 0;
+ }
+ .content {
+ padding: 16px;
+ }
+ .has-header {
+ padding-top: 0;
+ }
+ .cell-bullet {
+ width: 32px;
+ padding-right: 0;
+ }
+ .bullet {
+ border-width: 1px;
+ border-style: solid;
+ border-radius: 4px;
+ height: 16px;
+ width: 32px;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "hui-energy-sources-table-card": HuiEnergySourcesTableCard;
+ }
+}
diff --git a/src/panels/lovelace/cards/energy/hui-energy-summary-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts
similarity index 74%
rename from src/panels/lovelace/cards/energy/hui-energy-summary-graph-card.ts
rename to src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts
index 2e3eb96354..5c455a4865 100644
--- a/src/panels/lovelace/cards/energy/hui-energy-summary-graph-card.ts
+++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts
@@ -9,39 +9,34 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
-import { styleMap } from "lit/directives/style-map";
import {
hex2rgb,
lab2rgb,
rgb2hex,
rgb2lab,
} from "../../../../common/color/convert-color";
+import { hexBlend } from "../../../../common/color/hex";
import { labDarken } from "../../../../common/color/lab";
import { computeStateName } from "../../../../common/entity/compute_state_name";
-import { round } from "../../../../common/number/round";
-import { formatNumber } from "../../../../common/string/format_number";
+import {
+ formatNumber,
+ numberFormatToLocale,
+} from "../../../../common/string/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import { fetchStatistics, Statistics } from "../../../../data/history";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
-import { EnergySummaryGraphCardConfig } from "../types";
+import { EnergyUsageGraphCardConfig } from "../types";
-const NEGATIVE = ["to_grid"];
-const COLORS = {
- to_grid: { border: "#673ab7", background: "#b39bdb" },
- from_grid: { border: "#126A9A", background: "#8ab5cd" },
- used_solar: { border: "#FF9800", background: "#fecc8e" },
-};
-
-@customElement("hui-energy-summary-graph-card")
-export class HuiEnergySummaryGraphCard
+@customElement("hui-energy-usage-graph-card")
+export class HuiEnergyUsageGraphCard
extends LitElement
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
- @state() private _config?: EnergySummaryGraphCardConfig;
+ @state() private _config?: EnergyUsageGraphCardConfig;
@state() private _data?: Statistics;
@@ -81,7 +76,7 @@ export class HuiEnergySummaryGraphCard
return 3;
}
- public setConfig(config: EnergySummaryGraphCardConfig): void {
+ public setConfig(config: EnergyUsageGraphCardConfig): void {
this._config = config;
}
@@ -95,7 +90,7 @@ export class HuiEnergySummaryGraphCard
}
const oldConfig = changedProps.get("_config") as
- | EnergySummaryGraphCardConfig
+ | EnergyUsageGraphCardConfig
| undefined;
if (oldConfig !== this._config) {
@@ -116,42 +111,14 @@ export class HuiEnergySummaryGraphCard
return html`
-
+ ${this._config.title
+ ? html``
+ : ""}
-
-
- ${this._chartData.datasets.map(
- (dataset) => html`-
-
- ${formatNumber(
- Math.abs(
- dataset.data.reduce(
- (total, point) => total + (point as any).y,
- 0
- ) as number
- ),
- this.hass.locale
- )}
- kWh
-
`
- )}
-
-
Math.abs(round(value)),
+ callback: (value) =>
+ formatNumber(Math.abs(value), this.hass.locale),
},
},
},
@@ -218,7 +186,10 @@ export class HuiEnergySummaryGraphCard
filter: (val) => val.formattedValue !== "0",
callbacks: {
label: (context) =>
- `${context.dataset.label}: ${Math.abs(context.parsed.y)} kWh`,
+ `${context.dataset.label}: ${formatNumber(
+ Math.abs(context.parsed.y),
+ this.hass.locale
+ )} kWh`,
footer: (contexts) => {
let totalConsumed = 0;
let totalReturned = 0;
@@ -233,10 +204,16 @@ export class HuiEnergySummaryGraphCard
}
return [
totalConsumed
- ? `Total consumed: ${totalConsumed.toFixed(2)} kWh`
+ ? `Total consumed: ${formatNumber(
+ totalConsumed,
+ this.hass.locale
+ )} kWh`
: "",
totalReturned
- ? `Total returned: ${totalReturned.toFixed(2)} kWh`
+ ? `Total returned: ${formatNumber(
+ totalReturned,
+ this.hass.locale
+ )} kWh`
: "",
].filter(Boolean);
},
@@ -261,6 +238,8 @@ export class HuiEnergySummaryGraphCard
hitRadius: 5,
},
},
+ // @ts-expect-error
+ locale: numberFormatToLocale(this.hass.locale),
};
}
@@ -344,6 +323,23 @@ export class HuiEnergySummaryGraphCard
} = {};
const summedData: { [key: string]: { [start: string]: number } } = {};
+ const computedStyles = getComputedStyle(this);
+ const colors = {
+ to_grid: computedStyles
+ .getPropertyValue("--energy-grid-return-color")
+ .trim(),
+ from_grid: computedStyles
+ .getPropertyValue("--energy-grid-consumption-color")
+ .trim(),
+ used_solar: computedStyles
+ .getPropertyValue("--energy-solar-color")
+ .trim(),
+ };
+
+ const backgroundColor = computedStyles
+ .getPropertyValue("--card-background-color")
+ .trim();
+
Object.entries(statistics).forEach(([key, statIds]) => {
const sum = ["solar", "to_grid"].includes(key);
const add = key !== "solar";
@@ -407,12 +403,13 @@ export class HuiEnergySummaryGraphCard
const uniqueKeys = Array.from(new Set(allKeys));
Object.entries(combinedData).forEach(([type, sources]) => {
- const negative = NEGATIVE.includes(type);
-
Object.entries(sources).forEach(([statId, source], idx) => {
const data: ChartDataset<"bar">[] = [];
const entity = this.hass.states[statId];
- const color = COLORS[type];
+ const borderColor =
+ idx > 0
+ ? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(colors[type])), idx)))
+ : colors[type];
data.push({
label:
@@ -421,28 +418,20 @@ export class HuiEnergySummaryGraphCard
: entity
? computeStateName(entity)
: statId,
- borderColor:
- idx > 0
- ? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(color.border)), idx)))
- : color.border,
- backgroundColor:
- idx > 0
- ? rgb2hex(
- lab2rgb(labDarken(rgb2lab(hex2rgb(color.background)), idx))
- )
- : color.background,
+ borderColor,
+ backgroundColor: hexBlend(borderColor, backgroundColor, 50),
stack: "stack",
data: [],
});
// Process chart data.
for (const key of uniqueKeys) {
- const value = key in source ? Math.round(source[key] * 100) / 100 : 0;
+ const value = source[key] || 0;
const date = new Date(key);
// @ts-expect-error
data[0].data.push({
x: date.getTime(),
- y: value && negative ? -1 * value : value,
+ y: value && type === "to_grid" ? -1 * value : value,
});
}
@@ -470,43 +459,12 @@ export class HuiEnergySummaryGraphCard
.has-header {
padding-top: 0;
}
- .chartLegend ul {
- padding-left: 20px;
- }
- .chartLegend li {
- padding: 2px 8px;
- display: flex;
- justify-content: space-between;
- align-items: center;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- box-sizing: border-box;
- color: var(--secondary-text-color);
- }
- .chartLegend li > div {
- display: flex;
- align-items: center;
- }
- .chartLegend .bullet {
- border-width: 1px;
- border-style: solid;
- border-radius: 4px;
- display: inline-block;
- height: 16px;
- margin-right: 6px;
- width: 32px;
- box-sizing: border-box;
- }
- .value {
- font-weight: 300;
- }
`;
}
}
declare global {
interface HTMLElementTagNameMap {
- "hui-energy-summary-graph-card": HuiEnergySummaryGraphCard;
+ "hui-energy-usage-graph-card": HuiEnergyUsageGraphCard;
}
}
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index dcbf69054a..88684adfa8 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -101,7 +101,7 @@ export interface EnergyDistributionCardConfig extends LovelaceCardConfig {
title?: string;
prefs: EnergyPreferences;
}
-export interface EnergySummaryGraphCardConfig extends LovelaceCardConfig {
+export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig {
type: "energy-summary-graph";
title?: string;
prefs: EnergyPreferences;
@@ -119,6 +119,12 @@ export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
prefs: EnergyPreferences;
}
+export interface EnergySourcesTableCardConfig extends LovelaceCardConfig {
+ type: "energy-sources-table";
+ title?: string;
+ prefs: EnergyPreferences;
+}
+
export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig {
type: "energy-solar-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 71cee8f6fd..f6a489c098 100644
--- a/src/panels/lovelace/create-element/create-card-element.ts
+++ b/src/panels/lovelace/create-element/create-card-element.ts
@@ -35,14 +35,14 @@ 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-graph": () =>
- import("../cards/energy/hui-energy-summary-graph-card"),
+ "energy-usage-graph": () =>
+ import("../cards/energy/hui-energy-usage-graph-card"),
"energy-solar-graph": () =>
import("../cards/energy/hui-energy-solar-graph-card"),
"energy-devices-graph": () =>
import("../cards/energy/hui-energy-devices-graph-card"),
- "energy-costs-table": () =>
- import("../cards/energy/hui-energy-costs-table-card"),
+ "energy-sources-table": () =>
+ import("../cards/energy/hui-energy-sources-table-card"),
"energy-distribution": () =>
import("../cards/energy/hui-energy-distribution-card"),
"energy-solar-consumed-gauge": () =>
diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts
index a4ff54ff8f..9416b15f99 100644
--- a/src/resources/ha-style.ts
+++ b/src/resources/ha-style.ts
@@ -82,6 +82,14 @@ documentContainer.innerHTML = `
--state-climate-dry-color: #efbd07;
--state-climate-idle-color: #8a8a8a;
+ /* energy */
+ --energy-grid-consumption-color: #126a9a;
+ --energy-grid-return-color: #673ab7;
+ --energy-solar-color: #ff9800;
+ --energy-non-fossil-color: #0f9d58;
+
+ --rgb-energy-solar-color: 255, 152, 0;
+
/*
Paper-styles color.html dependency is stripped on build.
When a default paper-style color is used, it needs to be copied
diff --git a/src/resources/styles.ts b/src/resources/styles.ts
index f2b2494416..716de34179 100644
--- a/src/resources/styles.ts
+++ b/src/resources/styles.ts
@@ -31,6 +31,7 @@ export const darkStyles = {
"codemirror-property": "#C792EA",
"codemirror-qualifier": "#DECB6B",
"codemirror-type": "#DECB6B",
+ "energy-grid-return-color": "#b39bdb",
};
export const derivedStyles = {