Add water to energy dashboard (#14194)

This commit is contained in:
Bram Kragten 2022-10-26 20:44:07 +02:00 committed by GitHub
parent 7cc6809f53
commit 822917d060
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1401 additions and 49 deletions

View File

@ -52,6 +52,13 @@ export class HaStatisticPicker extends LitElement {
@property({ attribute: "include-unit-class" })
public includeUnitClass?: string | string[];
/**
* Show only statistics with these device classes.
* @attr include-device-class
*/
@property({ attribute: "include-device-class" })
public includeDeviceClass?: string | string[];
/**
* Show only statistics on entities.
* @type {Boolean}
@ -94,6 +101,7 @@ export class HaStatisticPicker extends LitElement {
statisticIds: StatisticsMetaData[],
includeStatisticsUnitOfMeasurement?: string | string[],
includeUnitClass?: string | string[],
includeDeviceClass?: string | string[],
entitiesOnly?: boolean
): Array<{ id: string; name: string; state?: HassEntity }> => {
if (!statisticIds.length) {
@ -122,6 +130,19 @@ export class HaStatisticPicker extends LitElement {
includeUnitClasses.includes(meta.unit_class)
);
}
if (includeDeviceClass) {
const includeDeviceClasses: (string | null)[] =
ensureArray(includeDeviceClass);
statisticIds = statisticIds.filter((meta) => {
const stateObj = this.hass.states[meta.statistic_id];
if (!stateObj) {
return true;
}
return includeDeviceClasses.includes(
stateObj.attributes.device_class || ""
);
});
}
const output: Array<{
id: string;
@ -195,6 +216,7 @@ export class HaStatisticPicker extends LitElement {
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly
);
} else {
@ -203,6 +225,7 @@ export class HaStatisticPicker extends LitElement {
this.statisticIds!,
this.includeStatisticsUnitOfMeasurement,
this.includeUnitClass,
this.includeDeviceClass,
this.entitiesOnly
);
});

View File

@ -38,6 +38,13 @@ class HaStatisticsPicker extends LitElement {
@property({ attribute: "include-unit-class" })
public includeUnitClass?: string | string[];
/**
* Show only statistics with these device classes.
* @attr include-device-class
*/
@property({ attribute: "include-device-class" })
public includeDeviceClass?: string | string[];
/**
* Ignore filtering of statistics type and units when only a single statistic is selected.
* @type {boolean}
@ -92,6 +99,7 @@ class HaStatisticsPicker extends LitElement {
.includeStatisticsUnitOfMeasurement=${this
.includeStatisticsUnitOfMeasurement}
.includeUnitClass=${this.includeUnitClass}
.includeDeviceClass=${this.includeDeviceClass}
.statisticTypes=${this.statisticTypes}
.statisticIds=${this.statisticIds}
.label=${this.pickStatisticLabel}

View File

@ -62,6 +62,7 @@ export const emptyBatteryEnergyPreference =
stat_energy_from: "",
stat_energy_to: "",
});
export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({
type: "gas",
stat_energy_from: "",
@ -70,6 +71,15 @@ export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({
number_energy_price: null,
});
export const emptyWaterEnergyPreference =
(): WaterSourceTypeEnergyPreference => ({
type: "water",
stat_energy_from: "",
stat_cost: null,
entity_energy_price: null,
number_energy_price: null,
});
interface EnergySolarForecast {
wh_hours: Record<string, number>;
}
@ -130,7 +140,22 @@ export interface BatterySourceTypeEnergyPreference {
export interface GasSourceTypeEnergyPreference {
type: "gas";
// kWh meter
// kWh/volume meter
stat_energy_from: string;
// $ meter
stat_cost: string | null;
// Can be used to generate costs if stat_cost omitted
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
}
export interface WaterSourceTypeEnergyPreference {
type: "water";
// volume meter
stat_energy_from: string;
// $ meter
@ -146,7 +171,8 @@ type EnergySource =
| SolarSourceTypeEnergyPreference
| GridSourceTypeEnergyPreference
| BatterySourceTypeEnergyPreference
| GasSourceTypeEnergyPreference;
| GasSourceTypeEnergyPreference
| WaterSourceTypeEnergyPreference;
export interface EnergyPreferences {
energy_sources: EnergySource[];
@ -222,6 +248,7 @@ interface EnergySourceByType {
solar?: SolarSourceTypeEnergyPreference[];
battery?: BatterySourceTypeEnergyPreference[];
gas?: GasSourceTypeEnergyPreference[];
water?: WaterSourceTypeEnergyPreference[];
}
export const energySourcesByType = (prefs: EnergyPreferences) =>
@ -255,7 +282,7 @@ export const getReferencedStatisticIds = (
continue;
}
if (source.type === "gas") {
if (source.type === "gas" || source.type === "water") {
statIDs.push(source.stat_energy_from);
if (source.stat_cost) {
statIDs.push(source.stat_cost);
@ -642,3 +669,6 @@ export const getEnergyGasUnit = (
? "m³"
: "ft³";
};
export const getEnergyWaterUnit = (hass: HomeAssistant): string | undefined =>
hass.config.unit_system.length === "km" ? "m³" : "ft³";

View File

@ -0,0 +1,204 @@
import "@material/mwc-button/mwc-button";
import { mdiDelete, mdiWater, mdiPencil } from "@mdi/js";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import "../../../../components/ha-icon-button";
import {
EnergyPreferences,
EnergyPreferencesValidation,
EnergyValidationIssue,
WaterSourceTypeEnergyPreference,
saveEnergyPreferences,
} from "../../../../data/energy";
import {
StatisticsMetaData,
getStatisticLabel,
} from "../../../../data/recorder";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { showEnergySettingsWaterDialog } from "../dialogs/show-dialogs-energy";
import "./ha-energy-validation-result";
import { energyCardStyles } from "./styles";
@customElement("ha-energy-water-settings")
export class EnergyWaterSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public preferences!: EnergyPreferences;
@property({ attribute: false })
public statsMetadata?: Record<string, StatisticsMetaData>;
@property({ attribute: false })
public validationResult?: EnergyPreferencesValidation;
protected render(): TemplateResult {
const waterSources: WaterSourceTypeEnergyPreference[] = [];
const waterValidation: EnergyValidationIssue[][] = [];
this.preferences.energy_sources.forEach((source, idx) => {
if (source.type !== "water") {
return;
}
waterSources.push(source);
if (this.validationResult) {
waterValidation.push(this.validationResult.energy_sources[idx]);
}
});
return html`
<ha-card outlined>
<h1 class="card-header">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
${this.hass.localize("ui.panel.config.energy.water.title")}
</h1>
<div class="card-content">
<p>
${this.hass.localize("ui.panel.config.energy.water.sub")}
<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(this.hass, "/docs/energy/water/")}
>${this.hass.localize(
"ui.panel.config.energy.water.learn_more"
)}</a
>
</p>
${waterValidation.map(
(result) =>
html`
<ha-energy-validation-result
.hass=${this.hass}
.issues=${result}
></ha-energy-validation-result>
`
)}
<h3>
${this.hass.localize(
"ui.panel.config.energy.water.water_consumption"
)}
</h3>
${waterSources.map((source) => {
const entityState = this.hass.states[source.stat_energy_from];
return html`
<div class="row" .source=${source}>
${entityState?.attributes.icon
? html`<ha-icon
.icon=${entityState.attributes.icon}
></ha-icon>`
: html`<ha-svg-icon .path=${mdiWater}></ha-svg-icon>`}
<span class="content"
>${getStatisticLabel(
this.hass,
source.stat_energy_from,
this.statsMetadata?.[source.stat_energy_from]
)}</span
>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.water.edit_water_source"
)}
@click=${this._editSource}
.path=${mdiPencil}
></ha-icon-button>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.water.delete_water_source"
)}
@click=${this._deleteSource}
.path=${mdiDelete}
></ha-icon-button>
</div>
`;
})}
<div class="row border-bottom">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
<mwc-button @click=${this._addSource}
>${this.hass.localize(
"ui.panel.config.energy.water.add_water_source"
)}</mwc-button
>
</div>
</div>
</ha-card>
`;
}
private _addSource() {
showEnergySettingsWaterDialog(this, {
saveCallback: async (source) => {
delete source.unit_of_measurement;
await this._savePreferences({
...this.preferences,
energy_sources: this.preferences.energy_sources.concat(source),
});
},
});
}
private _editSource(ev) {
const origSource: WaterSourceTypeEnergyPreference =
ev.currentTarget.closest(".row").source;
showEnergySettingsWaterDialog(this, {
source: { ...origSource },
metadata: this.statsMetadata?.[origSource.stat_energy_from],
saveCallback: async (newSource) => {
await this._savePreferences({
...this.preferences,
energy_sources: this.preferences.energy_sources.map((src) =>
src === origSource ? newSource : src
),
});
},
});
}
private async _deleteSource(ev) {
const sourceToDelete: WaterSourceTypeEnergyPreference =
ev.currentTarget.closest(".row").source;
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.energy.delete_source"),
}))
) {
return;
}
try {
await this._savePreferences({
...this.preferences,
energy_sources: this.preferences.energy_sources.filter(
(source) => source !== sourceToDelete
),
});
} catch (err: any) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
}
}
private async _savePreferences(preferences: EnergyPreferences) {
const result = await saveEnergyPreferences(this.hass, preferences);
fireEvent(this, "value-changed", { value: result });
}
static get styles(): CSSResultGroup {
return [haStyle, energyCardStyles];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-energy-water-settings": EnergyWaterSettings;
}
}

View File

@ -107,6 +107,7 @@ export class DialogEnergyGasSettings
"volume",
"energy",
]}
include-device-class="gas"
.value=${this._source.stat_energy_from}
.label=${`${this.hass.localize(
"ui.panel.config.energy.gas.dialog.gas_usage"

View File

@ -0,0 +1,281 @@
import "@material/mwc-button/mwc-button";
import { mdiWater } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import type { HaRadio } from "../../../../components/ha-radio";
import "../../../../components/ha-textfield";
import {
emptyWaterEnergyPreference,
WaterSourceTypeEnergyPreference,
} from "../../../../data/energy";
import { isExternalStatistic } from "../../../../data/recorder";
import { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { EnergySettingsWaterDialogParams } from "./show-dialogs-energy";
@customElement("dialog-energy-water-settings")
export class DialogEnergyWaterSettings
extends LitElement
implements HassDialog<EnergySettingsWaterDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EnergySettingsWaterDialogParams;
@state() private _source?: WaterSourceTypeEnergyPreference;
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
@state() private _error?: string;
public async showDialog(
params: EnergySettingsWaterDialogParams
): Promise<void> {
this._params = params;
this._source = params.source
? { ...params.source }
: emptyWaterEnergyPreference();
this._costs = this._source.entity_energy_price
? "entity"
: this._source.number_energy_price
? "number"
: this._source.stat_cost
? "statistic"
: "no-costs";
}
public closeDialog(): void {
this._params = undefined;
this._source = undefined;
this._error = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params || !this._source) {
return html``;
}
const externalSource =
this._source.stat_cost && isExternalStatistic(this._source.stat_cost);
return html`
<ha-dialog
open
.heading=${html`<ha-svg-icon
.path=${mdiWater}
style="--mdc-icon-size: 32px;"
></ha-svg-icon>
${this.hass.localize("ui.panel.config.energy.water.dialog.header")}`}
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<ha-statistic-picker
.hass=${this.hass}
include-unit-class="volume"
include-device-class="water"
.value=${this._source.stat_energy_from}
.label=${this.hass.localize(
"ui.panel.config.energy.water.dialog.water_usage"
)}
@value-changed=${this._statisticChanged}
dialogInitialFocus
></ha-statistic-picker>
<p>
${this.hass.localize(`ui.panel.config.energy.water.dialog.cost_para`)}
</p>
<ha-formfield
.label=${this.hass.localize(
`ui.panel.config.energy.water.dialog.no_cost`
)}
>
<ha-radio
value="no-costs"
name="costs"
.checked=${this._costs === "no-costs"}
@change=${this._handleCostChanged}
></ha-radio>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
`ui.panel.config.energy.water.dialog.cost_stat`
)}
>
<ha-radio
value="statistic"
name="costs"
.checked=${this._costs === "statistic"}
.disabled=${externalSource}
@change=${this._handleCostChanged}
></ha-radio>
</ha-formfield>
${this._costs === "statistic"
? html`<ha-statistic-picker
class="price-options"
.hass=${this.hass}
statistic-types="sum"
.value=${this._source.stat_cost}
.label=${this.hass.localize(
`ui.panel.config.energy.water.dialog.cost_stat_input`
)}
@value-changed=${this._priceStatChanged}
></ha-statistic-picker>`
: ""}
<ha-formfield
.label=${this.hass.localize(
`ui.panel.config.energy.water.dialog.cost_entity`
)}
>
<ha-radio
value="entity"
name="costs"
.checked=${this._costs === "entity"}
.disabled=${externalSource}
@change=${this._handleCostChanged}
></ha-radio>
</ha-formfield>
${this._costs === "entity"
? html`<ha-entity-picker
class="price-options"
.hass=${this.hass}
include-domains='["sensor", "input_number"]'
.value=${this._source.entity_energy_price}
.label=${this.hass.localize(
`ui.panel.config.energy.water.dialog.cost_entity_input`
)}
@value-changed=${this._priceEntityChanged}
></ha-entity-picker>`
: ""}
<ha-formfield
.label=${this.hass.localize(
`ui.panel.config.energy.water.dialog.cost_number`
)}
>
<ha-radio
value="number"
name="costs"
.checked=${this._costs === "number"}
@change=${this._handleCostChanged}
></ha-radio>
</ha-formfield>
${this._costs === "number"
? html`<ha-textfield
.label=${this.hass.localize(
`ui.panel.config.energy.water.dialog.cost_number_input`
)}
class="price-options"
step=".01"
type="number"
.value=${this._source.number_energy_price}
@change=${this._numberPriceChanged}
.suffix=${`${this.hass.config.currency}/m³`}
>
</ha-textfield>`
: ""}
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</mwc-button>
<mwc-button
@click=${this._save}
.disabled=${!this._source.stat_energy_from}
slot="primaryAction"
>
${this.hass.localize("ui.common.save")}
</mwc-button>
</ha-dialog>
`;
}
private _handleCostChanged(ev: CustomEvent) {
const input = ev.currentTarget as HaRadio;
this._costs = input.value as any;
}
private _numberPriceChanged(ev) {
this._source = {
...this._source!,
number_energy_price: Number(ev.target.value),
entity_energy_price: null,
stat_cost: null,
};
}
private _priceStatChanged(ev: CustomEvent) {
this._source = {
...this._source!,
entity_energy_price: null,
number_energy_price: null,
stat_cost: ev.detail.value,
};
}
private _priceEntityChanged(ev: CustomEvent) {
this._source = {
...this._source!,
entity_energy_price: ev.detail.value,
number_energy_price: null,
stat_cost: null,
};
}
private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (isExternalStatistic(ev.detail.value) && this._costs !== "statistic") {
this._costs = "no-costs";
}
this._source = {
...this._source!,
stat_energy_from: ev.detail.value,
};
}
private async _save() {
try {
if (this._costs === "no-costs") {
this._source!.entity_energy_price = null;
this._source!.number_energy_price = null;
this._source!.stat_cost = null;
}
await this._params!.saveCallback(this._source!);
this.closeDialog();
} catch (err: any) {
this._error = err.message;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 430px;
}
ha-formfield {
display: block;
}
.price-options {
display: block;
padding-left: 52px;
margin-top: -8px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-energy-water-settings": DialogEnergyWaterSettings;
}
}

View File

@ -8,6 +8,7 @@ import {
FlowToGridSourceEnergyPreference,
GasSourceTypeEnergyPreference,
SolarSourceTypeEnergyPreference,
WaterSourceTypeEnergyPreference,
} from "../../../../data/energy";
import { StatisticsMetaData } from "../../../../data/recorder";
@ -51,6 +52,12 @@ export interface EnergySettingsGasDialogParams {
saveCallback: (source: GasSourceTypeEnergyPreference) => Promise<void>;
}
export interface EnergySettingsWaterDialogParams {
source?: WaterSourceTypeEnergyPreference;
metadata?: StatisticsMetaData;
saveCallback: (source: WaterSourceTypeEnergyPreference) => Promise<void>;
}
export interface EnergySettingsDeviceDialogParams {
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
}
@ -99,6 +106,17 @@ export const showEnergySettingsGasDialog = (
});
};
export const showEnergySettingsWaterDialog = (
element: HTMLElement,
dialogParams: EnergySettingsWaterDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-energy-water-settings",
dialogImport: () => import("./dialog-energy-water-settings"),
dialogParams: dialogParams,
});
};
export const showEnergySettingsGridFlowFromDialog = (
element: HTMLElement,
dialogParams: EnergySettingsGridFlowFromDialogParams

View File

@ -24,6 +24,7 @@ import "./components/ha-energy-grid-settings";
import "./components/ha-energy-solar-settings";
import "./components/ha-energy-battery-settings";
import "./components/ha-energy-gas-settings";
import "./components/ha-energy-water-settings";
const INITIAL_CONFIG: EnergyPreferences = {
energy_sources: [],
@ -116,6 +117,13 @@ class HaConfigEnergy extends LitElement {
.validationResult=${this._validationResult}
@value-changed=${this._prefsChanged}
></ha-energy-gas-settings>
<ha-energy-water-settings
.hass=${this.hass}
.preferences=${this._preferences!}
.statsMetadata=${this._statsMetadata}
.validationResult=${this._validationResult}
@value-changed=${this._prefsChanged}
></ha-energy-water-settings>
<ha-energy-device-settings
.hass=${this.hass}
.preferences=${this._preferences!}

View File

@ -15,6 +15,7 @@ import "../../config/energy/components/ha-energy-grid-settings";
import "../../config/energy/components/ha-energy-solar-settings";
import "../../config/energy/components/ha-energy-battery-settings";
import "../../config/energy/components/ha-energy-gas-settings";
import "../../config/energy/components/ha-energy-water-settings";
import "../../config/energy/components/ha-energy-device-settings";
import { haStyle } from "../../../resources/styles";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
@ -54,7 +55,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
<p>
${this.hass.localize("ui.panel.energy.setup.step", {
step: this._step + 1,
steps: 5,
steps: 6,
})}
</p>
${this._step === 0
@ -82,6 +83,12 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
.preferences=${this._preferences}
@value-changed=${this._prefsChanged}
></ha-energy-gas-settings>`
: this._step === 4
? html`<ha-energy-water-settings
.hass=${this.hass}
.preferences=${this._preferences}
@value-changed=${this._prefsChanged}
></ha-energy-water-settings>`
: html`<ha-energy-device-settings
.hass=${this.hass}
.preferences=${this._preferences}
@ -120,7 +127,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
}
private _next() {
if (this._step === 4) {
if (this._step === 5) {
return;
}
this._step++;

View File

@ -52,6 +52,10 @@ export class EnergyStrategy {
);
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
const hasWater = prefs.energy_sources.some(
(source) => source.type === "water"
);
if (info.narrow) {
view.cards!.push({
type: "energy-date-selection",
@ -92,6 +96,15 @@ export class EnergyStrategy {
});
}
// Only include if we have a water source.
if (hasWater) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
type: "energy-water-graph",
collection_key: "energy_dashboard",
});
}
// Only include if we have a grid.
if (hasGrid) {
view.cards!.push({

View File

@ -1,3 +1,4 @@
import "@material/mwc-button";
import {
mdiArrowDown,
mdiArrowLeft,
@ -9,12 +10,12 @@ import {
mdiLeaf,
mdiSolarPower,
mdiTransmissionTower,
mdiWater,
} from "@mdi/js";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, svg } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "@material/mwc-button";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/ha-card";
import "../../../../components/ha-svg-icon";
@ -23,6 +24,7 @@ import {
energySourcesByType,
getEnergyDataCollection,
getEnergyGasUnit,
getEnergyWaterUnit,
} from "../../../../data/energy";
import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@ -83,6 +85,7 @@ class HuiEnergyDistrubutionCard
const hasSolarProduction = types.solar !== undefined;
const hasBattery = types.battery !== undefined;
const hasGas = types.gas !== undefined;
const hasWater = types.water !== undefined;
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
const totalFromGrid =
@ -91,6 +94,15 @@ class HuiEnergyDistrubutionCard
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
) ?? 0;
let waterUsage: number | null = null;
if (hasWater) {
waterUsage =
calculateStatisticsSumGrowth(
this._data.stats,
types.water!.map((source) => source.stat_energy_from)
) ?? 0;
}
let gasUsage: number | null = null;
if (hasGas) {
gasUsage =
@ -255,7 +267,10 @@ class HuiEnergyDistrubutionCard
return html`
<ha-card .header=${this._config.title}>
<div class="card-content">
${lowCarbonEnergy !== undefined || hasSolarProduction || hasGas
${lowCarbonEnergy !== undefined ||
hasSolarProduction ||
hasGas ||
hasWater
? html`<div class="row">
${lowCarbonEnergy === undefined
? html`<div class="spacer"></div>`
@ -298,7 +313,7 @@ class HuiEnergyDistrubutionCard
kWh
</div>
</div>`
: hasGas
: hasGas || hasWater
? html`<div class="spacer"></div>`
: ""}
${hasGas
@ -338,6 +353,39 @@ class HuiEnergyDistrubutionCard
: ""}
</svg>
</div>`
: hasWater
? html`<div class="circle-container water">
<span class="label"
>${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.water"
)}</span
>
<div class="circle">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
${formatNumber(waterUsage || 0, this.hass.locale, {
maximumFractionDigits: 1,
})}
${getEnergyWaterUnit(this.hass) || "m³"}
</div>
<svg width="80" height="30">
<path d="M40 0 v30" id="water" />
${waterUsage
? svg`<circle
r="1"
class="water"
vector-effect="non-scaling-stroke"
>
<animateMotion
dur="2s"
repeatCount="indefinite"
calcMode="linear"
>
<mpath xlink:href="#water" />
</animateMotion>
</circle>`
: ""}
</svg>
</div>`
: html`<div class="spacer"></div>`}
</div>`
: ""}
@ -460,50 +508,99 @@ class HuiEnergyDistrubutionCard
</svg>`
: ""}
</div>
<span class="label"
>${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.home"
)}</span
>
${hasGas && hasWater
? ""
: html`<span class="label"
>${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.home"
)}</span
>`}
</div>
</div>
${hasBattery
${hasBattery || (hasGas && hasWater)
? html`<div class="row">
<div class="spacer"></div>
<div class="circle-container battery">
<div class="circle">
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
<span class="battery-in">
<ha-svg-icon
class="small"
.path=${mdiArrowDown}
></ha-svg-icon
>${formatNumber(totalBatteryIn || 0, this.hass.locale, {
maximumFractionDigits: 1,
})}
kWh</span
>
<span class="battery-out">
<ha-svg-icon
class="small"
.path=${mdiArrowUp}
></ha-svg-icon
>${formatNumber(totalBatteryOut || 0, this.hass.locale, {
maximumFractionDigits: 1,
})}
kWh</span
>
</div>
<span class="label"
>${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
)}</span
${hasBattery
? html` <div class="circle-container battery">
<div class="circle">
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
<span class="battery-in">
<ha-svg-icon
class="small"
.path=${mdiArrowDown}
></ha-svg-icon
>${formatNumber(
totalBatteryIn || 0,
this.hass.locale,
{
maximumFractionDigits: 1,
}
)}
kWh</span
>
<span class="battery-out">
<ha-svg-icon
class="small"
.path=${mdiArrowUp}
></ha-svg-icon
>${formatNumber(
totalBatteryOut || 0,
this.hass.locale,
{
maximumFractionDigits: 1,
}
)}
kWh</span
>
</div>
<span class="label"
>${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
)}</span
>
</div>`
: html`<div class="spacer"></div>`}
${hasGas && hasWater
? html`<div class="circle-container water bottom">
<svg width="80" height="30">
<path d="M40 30 v-30" id="water" />
${waterUsage
? svg`<circle
r="1"
class="water"
vector-effect="non-scaling-stroke"
>
</div>
<div class="spacer"></div>
<animateMotion
dur="2s"
repeatCount="indefinite"
calcMode="linear"
>
<mpath xlink:href="#water" />
</animateMotion>
</circle>`
: ""}
</svg>
<div class="circle">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
${formatNumber(waterUsage || 0, this.hass.locale, {
maximumFractionDigits: 1,
})}
${getEnergyWaterUnit(this.hass) || "m³"}
</div>
<span class="label"
>${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_distribution.water"
)}</span
>
</div>`
: html`<div class="spacer"></div>`}
</div>`
: ""}
<div class="lines ${classMap({ battery: hasBattery })}">
<div
class="lines ${classMap({
high: hasBattery || (hasGas && hasWater),
})}"
>
<svg
viewBox="0 0 100 100"
xmlns="http://www.w3.org/2000/svg"
@ -713,7 +810,7 @@ class HuiEnergyDistrubutionCard
padding: 0 16px 16px;
box-sizing: border-box;
}
.lines.battery {
.lines.high {
bottom: 100px;
height: 156px;
}
@ -744,6 +841,15 @@ class HuiEnergyDistrubutionCard
margin-left: 4px;
height: 130px;
}
.circle-container.water {
margin-left: 4px;
height: 130px;
}
.circle-container.water.bottom {
position: relative;
top: -20px;
margin-bottom: -20px;
}
.circle-container.battery {
height: 110px;
justify-content: flex-end;
@ -777,6 +883,7 @@ class HuiEnergyDistrubutionCard
.label {
color: var(--secondary-text-color);
font-size: 12px;
opacity: 1;
}
line,
path {
@ -804,6 +911,17 @@ class HuiEnergyDistrubutionCard
.gas .circle {
border-color: var(--energy-gas-color);
}
.water path,
.water circle {
stroke: var(--energy-water-color);
}
circle.water {
stroke-width: 4;
fill: var(--energy-water-color);
}
.water .circle {
border-color: var(--energy-water-color);
}
.low-carbon line {
stroke: var(--energy-non-fossil-color);
}

View File

@ -25,6 +25,7 @@ import {
energySourcesByType,
getEnergyDataCollection,
getEnergyGasUnit,
getEnergyWaterUnit,
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
@ -83,6 +84,8 @@ export class HuiEnergySourcesTableCard
let totalBattery = 0;
let totalGas = 0;
let totalGasCost = 0;
let totalWater = 0;
let totalWaterCost = 0;
let totalGridCompare = 0;
let totalGridCostCompare = 0;
@ -90,6 +93,8 @@ export class HuiEnergySourcesTableCard
let totalBatteryCompare = 0;
let totalGasCompare = 0;
let totalGasCostCompare = 0;
let totalWaterCompare = 0;
let totalWaterCostCompare = 0;
const types = energySourcesByType(this._data.prefs);
@ -112,6 +117,9 @@ export class HuiEnergySourcesTableCard
const gasColor = computedStyles
.getPropertyValue("--energy-gas-color")
.trim();
const waterColor = computedStyles
.getPropertyValue("--energy-water-color")
.trim();
const showCosts =
types.grid?.[0].flow_from.some(
@ -127,12 +135,18 @@ export class HuiEnergySourcesTableCard
types.gas?.some(
(flow) =>
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
) ||
types.water?.some(
(flow) =>
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
);
const gasUnit =
getEnergyGasUnit(this.hass, this._data.prefs, this._data.statsMetadata) ||
"";
const waterUnit = getEnergyWaterUnit(this.hass) || "m³";
const compare = this._data.statsCompare !== undefined;
return html` <ha-card>
@ -851,7 +865,157 @@ export class HuiEnergySourcesTableCard
: ""}
</tr>`
: ""}
${totalGasCost && totalGridCost
${types.water?.map((source, idx) => {
const energy =
calculateStatisticSumGrowth(
this._data!.stats[source.stat_energy_from]
) || 0;
totalWater += energy;
const energyCompare =
(compare &&
calculateStatisticSumGrowth(
this._data!.statsCompare[source.stat_energy_from]
)) ||
0;
totalWaterCompare += energyCompare;
const cost_stat =
source.stat_cost ||
this._data!.info.cost_sensors[source.stat_energy_from];
const cost = cost_stat
? calculateStatisticSumGrowth(this._data!.stats[cost_stat]) ||
0
: null;
if (cost !== null) {
totalWaterCost += cost;
}
const costCompare =
compare && cost_stat
? calculateStatisticSumGrowth(
this._data!.statsCompare[cost_stat]
) || 0
: null;
if (costCompare !== null) {
totalWaterCostCompare += costCompare;
}
const modifiedColor =
idx > 0
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(waterColor)), idx)
: labDarken(rgb2lab(hex2rgb(waterColor)), idx)
: undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: waterColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
class="bullet"
style=${styleMap({
borderColor: color,
backgroundColor: color + "7F",
})}
></div>
</td>
<th class="mdc-data-table__cell" scope="row">
${getStatisticLabel(
this.hass,
source.stat_energy_from,
this._data?.statsMetadata[source.stat_energy_from]
)}
</th>
${compare
? html` <td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(energyCompare, this.hass.locale)}
${waterUnit}
</td>
${showCosts
? html`<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${costCompare !== null
? formatNumber(costCompare, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})
: ""}
</td>`
: ""}`
: ""}
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(energy, this.hass.locale)} ${waterUnit}
</td>
${showCosts
? html`<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${cost !== null
? formatNumber(cost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})
: ""}
</td>`
: ""}
</tr>`;
})}
${types.water
? html`<tr class="mdc-data-table__row total">
<td class="mdc-data-table__cell"></td>
<th class="mdc-data-table__cell" scope="row">
${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_sources_table.water_total"
)}
</th>
${compare
? html`<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(totalWaterCompare, this.hass.locale)}
${gasUnit}
</td>
${showCosts
? html`<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(
totalWaterCostCompare,
this.hass.locale,
{
style: "currency",
currency: this.hass.config.currency!,
}
)}
</td>`
: ""}`
: ""}
<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(totalWater, this.hass.locale)} ${waterUnit}
</td>
${showCosts
? html`<td
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(totalWaterCost, this.hass.locale, {
style: "currency",
currency: this.hass.config.currency!,
})}
</td>`
: ""}
</tr>`
: ""}
${[totalGasCost, totalWaterCost, totalGridCost].filter(Boolean)
.length > 1
? html`<tr class="mdc-data-table__row total">
<td class="mdc-data-table__cell"></td>
<th class="mdc-data-table__cell" scope="row">
@ -867,7 +1031,9 @@ export class HuiEnergySourcesTableCard
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(
totalGasCostCompare + totalGridCostCompare,
totalGasCostCompare +
totalGridCostCompare +
totalWaterCostCompare,
this.hass.locale,
{
style: "currency",
@ -881,7 +1047,7 @@ export class HuiEnergySourcesTableCard
class="mdc-data-table__cell mdc-data-table__cell--numeric"
>
${formatNumber(
totalGasCost + totalGridCost,
totalGasCost + totalGridCost + totalWaterCost,
this.hass.locale,
{
style: "currency",
@ -922,6 +1088,7 @@ export class HuiEnergySourcesTableCard
}
ha-card {
height: 100%;
overflow: hidden;
}
.card-header {
padding-bottom: 0;

View File

@ -0,0 +1,431 @@
import {
ChartData,
ChartDataset,
ChartOptions,
ScatterDataPoint,
} from "chart.js";
import {
addHours,
differenceInDays,
differenceInHours,
endOfToday,
isToday,
startOfToday,
} from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import {
hex2rgb,
lab2rgb,
rgb2hex,
rgb2lab,
} from "../../../../common/color/convert-color";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatDateShort } from "../../../../common/datetime/format_date";
import { formatTime } from "../../../../common/datetime/format_time";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
EnergyData,
WaterSourceTypeEnergyPreference,
getEnergyDataCollection,
getEnergyWaterUnit,
} from "../../../../data/energy";
import {
Statistics,
StatisticsMetaData,
getStatisticLabel,
} from "../../../../data/recorder";
import { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergyWaterGraphCardConfig } from "../types";
@customElement("hui-energy-water-graph-card")
export class HuiEnergyWaterGraphCard
extends SubscribeMixin(LitElement)
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: EnergyWaterGraphCardConfig;
@state() private _chartData: ChartData = {
datasets: [],
};
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@state() private _compareStart?: Date;
@state() private _compareEnd?: Date;
@state() private _unit?: string;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {
key: this._config?.collection_key,
}).subscribe((data) => this._getStatistics(data)),
];
}
public getCardSize(): Promise<number> | number {
return 3;
}
public setConfig(config: EnergyWaterGraphCardConfig): void {
this._config = config;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
}
return html`
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
})}"
>
<ha-chart-base
.data=${this._chartData}
.options=${this._createOptions(
this._start,
this._end,
this.hass.locale,
this._unit,
this._compareStart,
this._compareEnd
)}
chart-type="bar"
></ha-chart-base>
${!this._chartData.datasets.length
? html`<div class="no-data">
${isToday(this._start)
? this.hass.localize("ui.panel.lovelace.cards.energy.no_data")
: this.hass.localize(
"ui.panel.lovelace.cards.energy.no_data_period"
)}
</div>`
: ""}
</div>
</ha-card>
`;
}
private _createOptions = memoizeOne(
(
start: Date,
end: Date,
locale: FrontendLocaleData,
unit?: string,
compareStart?: Date,
compareEnd?: Date
): ChartOptions => {
const dayDifference = differenceInDays(end, start);
const compare = compareStart !== undefined && compareEnd !== undefined;
if (compare) {
const difference = differenceInHours(end, start);
const differenceCompare = differenceInHours(compareEnd!, compareStart!);
// If the compare period doesn't match the main period, adjust them to match
if (differenceCompare > difference) {
end = addHours(end, differenceCompare - difference);
} else if (difference > differenceCompare) {
compareEnd = addHours(compareEnd!, difference - differenceCompare);
}
}
const options: ChartOptions = {
parsing: false,
animation: false,
scales: {
x: {
type: "time",
suggestedMin: start.getTime(),
suggestedMax: end.getTime(),
adapters: {
date: {
locale: locale,
},
},
ticks: {
maxRotation: 0,
sampleSize: 5,
autoSkipPadding: 20,
major: {
enabled: true,
},
font: (context) =>
context.tick && context.tick.major
? ({ weight: "bold" } as any)
: {},
},
time: {
tooltipFormat:
dayDifference > 35
? "monthyear"
: dayDifference > 7
? "date"
: dayDifference > 2
? "weekday"
: dayDifference > 0
? "datetime"
: "hour",
minUnit:
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour",
},
offset: true,
},
y: {
stacked: true,
type: "linear",
title: {
display: true,
text: unit,
},
ticks: {
beginAtZero: true,
},
},
},
plugins: {
tooltip: {
mode: "nearest",
callbacks: {
title: (datasets) => {
if (dayDifference > 0) {
return datasets[0].label;
}
const date = new Date(datasets[0].parsed.x);
return `${
compare ? `${formatDateShort(date, locale)}: ` : ""
}${formatTime(date, locale)} ${formatTime(
addHours(date, 1),
locale
)}`;
},
label: (context) =>
`${context.dataset.label}: ${formatNumber(
context.parsed.y,
locale
)} ${unit}`,
},
},
filler: {
propagate: false,
},
legend: {
display: false,
labels: {
usePointStyle: true,
},
},
},
hover: {
mode: "nearest",
},
elements: {
bar: { borderWidth: 1.5, borderRadius: 4 },
point: {
hitRadius: 5,
},
},
// @ts-expect-error
locale: numberFormatToLocale(locale),
};
if (compare) {
options.scales!.xAxisCompare = {
...(options.scales!.x as Record<string, any>),
suggestedMin: compareStart!.getTime(),
suggestedMax: compareEnd!.getTime(),
display: false,
};
}
return options;
}
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
const waterSources: WaterSourceTypeEnergyPreference[] =
energyData.prefs.energy_sources.filter(
(source) => source.type === "water"
) as WaterSourceTypeEnergyPreference[];
this._unit = getEnergyWaterUnit(this.hass);
const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
const computedStyles = getComputedStyle(this);
const waterColor = computedStyles
.getPropertyValue("--energy-water-color")
.trim();
datasets.push(
...this._processDataSet(
energyData.stats,
energyData.statsMetadata,
waterSources,
waterColor
)
);
if (energyData.statsCompare) {
// Add empty dataset to align the bars
datasets.push({
order: 0,
data: [],
});
datasets.push({
order: 999,
data: [],
xAxisID: "xAxisCompare",
});
datasets.push(
...this._processDataSet(
energyData.statsCompare,
energyData.statsMetadata,
waterSources,
waterColor,
true
)
);
}
this._start = energyData.start;
this._end = energyData.end || endOfToday();
this._compareStart = energyData.startCompare;
this._compareEnd = energyData.endCompare;
this._chartData = {
datasets,
};
}
private _processDataSet(
statistics: Statistics,
statisticsMetaData: Record<string, StatisticsMetaData>,
waterSources: WaterSourceTypeEnergyPreference[],
waterColor: string,
compare = false
) {
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
waterSources.forEach((source, idx) => {
const modifiedColor =
idx > 0
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(waterColor)), idx)
: labDarken(rgb2lab(hex2rgb(waterColor)), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: waterColor;
let prevValue: number | null = null;
let prevStart: string | null = null;
const waterConsumptionData: ScatterDataPoint[] = [];
// Process water consumption data.
if (source.stat_energy_from in statistics) {
const stats = statistics[source.stat_energy_from];
for (const point of stats) {
if (point.sum === null) {
continue;
}
if (prevValue === null) {
prevValue = point.sum;
continue;
}
if (prevStart === point.start) {
continue;
}
const value = point.sum - prevValue;
const date = new Date(point.start);
waterConsumptionData.push({
x: date.getTime(),
y: value,
});
prevStart = point.start;
prevValue = point.sum;
}
}
data.push({
label: getStatisticLabel(
this.hass,
source.stat_energy_from,
statisticsMetaData[source.stat_energy_from]
),
borderColor: compare ? borderColor + "7F" : borderColor,
backgroundColor: compare ? borderColor + "32" : borderColor + "7F",
data: waterConsumptionData,
order: 1,
stack: "water",
xAxisID: compare ? "xAxisCompare" : undefined,
});
});
return data;
}
static get styles(): CSSResultGroup {
return css`
ha-card {
height: 100%;
}
.card-header {
padding-bottom: 0;
}
.content {
padding: 16px;
}
.has-header {
padding-top: 0;
}
.no-data {
position: absolute;
height: 100%;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
padding: 20%;
margin-left: 32px;
box-sizing: border-box;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-energy-water-graph-card": HuiEnergyWaterGraphCard;
}
}

View File

@ -133,6 +133,12 @@ export interface EnergyGasGraphCardConfig extends LovelaceCardConfig {
collection_key?: string;
}
export interface EnergyWaterGraphCardConfig extends LovelaceCardConfig {
type: "energy-water-graph";
title?: string;
collection_key?: string;
}
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
type: "energy-devices-graph";
title?: string;

View File

@ -47,6 +47,8 @@ const LAZY_LOAD_TYPES = {
"energy-distribution": () =>
import("../cards/energy/hui-energy-distribution-card"),
"energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-card"),
"energy-water-graph": () =>
import("../cards/energy/hui-energy-water-graph-card"),
"energy-grid-neutrality-gauge": () =>
import("../cards/energy/hui-energy-grid-neutrality-gauge-card"),
"energy-solar-consumed-gauge": () =>

View File

@ -91,6 +91,7 @@ documentContainer.innerHTML = `<custom-style>
--energy-battery-out-color: #4db6ac;
--energy-battery-in-color: #f06292;
--energy-gas-color: #8E021B;
--energy-water-color: #00bcd4;
/* opacity for dark text on a light background */
--dark-divider-opacity: 0.12;

View File

@ -1500,6 +1500,29 @@
"m3_or_kWh": "ft³, m³, Wh, kWh, MWh or GJ"
}
},
"water": {
"title": "Water Consumption",
"sub": "Let Home Assistant monitor your water usage.",
"learn_more": "More information on how to get started.",
"water_consumption": "Water consumption",
"edit_water_source": "Edit water source",
"delete_water_source": "Delete water source",
"add_water_source": "Add water source",
"dialog": {
"header": "Configure water consumption",
"paragraph": "Gas consumption is the volume of water that flows to your home.",
"energy_stat": "Consumed water (m³/ft³)",
"cost_para": "Select how Home Assistant should keep track of the costs of the consumed water.",
"no_cost": "Do not track costs",
"cost_stat": "Use an entity tracking the total costs",
"cost_stat_input": "Total Costs Entity",
"cost_entity": "Use an entity with current price",
"cost_entity_input": "Entity with the current price per m³",
"cost_number": "Use a static price",
"cost_number_input": "Price per m³",
"water_usage": "Water usage (m³/ft³)"
}
},
"device_consumption": {
"title": "Individual devices",
"sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",
@ -1544,6 +1567,10 @@
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh', 'MWh' or 'GJ' for an energy sensor or 'm³' or 'ft³' for a gas sensor:"
},
"entity_unexpected_unit_water": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement 'm³' or 'ft³' for a water sensor:"
},
"entity_unexpected_unit_energy_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'' or ''{currency}/GJ'':"
@ -1552,6 +1579,10 @@
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'', ''{currency}/GJ'', ''{currency}/m³'' or ''{currency}/ft³'':"
},
"entity_unexpected_unit_water_price": {
"title": "Unexpected unit of measurement",
"description": "The following entities do not have the expected units of measurement ''{currency}/m³'' or ''{currency}/ft³'':"
},
"entity_unexpected_state_class": {
"title": "Unexpected state class",
"description": "The following entities do not have the expected state class:"
@ -3681,6 +3712,7 @@
"energy_sources_table": {
"grid_total": "Grid total",
"gas_total": "Gas total",
"water_total": "Water total",
"source": "Source",
"energy": "Energy",
"cost": "Cost",
@ -3711,6 +3743,7 @@
"title_today": "Energy distribution today",
"grid": "Grid",
"gas": "Gas",
"water": "Water",
"solar": "Solar",
"low_carbon": "Low-carbon",
"home": "Home",
@ -4773,6 +4806,7 @@
"energy_usage_graph_title": "Energy usage",
"energy_solar_graph_title": "Solar production",
"energy_gas_graph_title": "Gas consumption",
"energy_water_graph_title": "Water consumption",
"energy_distribution_title": "Energy distribution",
"energy_sources_table_title": "Sources",
"energy_devices_graph_title": "Monitor individual devices"