mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add water to energy dashboard (#14194)
This commit is contained in:
parent
7cc6809f53
commit
822917d060
@ -52,6 +52,13 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
@property({ attribute: "include-unit-class" })
|
@property({ attribute: "include-unit-class" })
|
||||||
public includeUnitClass?: string | string[];
|
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.
|
* Show only statistics on entities.
|
||||||
* @type {Boolean}
|
* @type {Boolean}
|
||||||
@ -94,6 +101,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
statisticIds: StatisticsMetaData[],
|
statisticIds: StatisticsMetaData[],
|
||||||
includeStatisticsUnitOfMeasurement?: string | string[],
|
includeStatisticsUnitOfMeasurement?: string | string[],
|
||||||
includeUnitClass?: string | string[],
|
includeUnitClass?: string | string[],
|
||||||
|
includeDeviceClass?: string | string[],
|
||||||
entitiesOnly?: boolean
|
entitiesOnly?: boolean
|
||||||
): Array<{ id: string; name: string; state?: HassEntity }> => {
|
): Array<{ id: string; name: string; state?: HassEntity }> => {
|
||||||
if (!statisticIds.length) {
|
if (!statisticIds.length) {
|
||||||
@ -122,6 +130,19 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
includeUnitClasses.includes(meta.unit_class)
|
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<{
|
const output: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@ -195,6 +216,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
this.statisticIds!,
|
this.statisticIds!,
|
||||||
this.includeStatisticsUnitOfMeasurement,
|
this.includeStatisticsUnitOfMeasurement,
|
||||||
this.includeUnitClass,
|
this.includeUnitClass,
|
||||||
|
this.includeDeviceClass,
|
||||||
this.entitiesOnly
|
this.entitiesOnly
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -203,6 +225,7 @@ export class HaStatisticPicker extends LitElement {
|
|||||||
this.statisticIds!,
|
this.statisticIds!,
|
||||||
this.includeStatisticsUnitOfMeasurement,
|
this.includeStatisticsUnitOfMeasurement,
|
||||||
this.includeUnitClass,
|
this.includeUnitClass,
|
||||||
|
this.includeDeviceClass,
|
||||||
this.entitiesOnly
|
this.entitiesOnly
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -38,6 +38,13 @@ class HaStatisticsPicker extends LitElement {
|
|||||||
@property({ attribute: "include-unit-class" })
|
@property({ attribute: "include-unit-class" })
|
||||||
public includeUnitClass?: string | string[];
|
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.
|
* Ignore filtering of statistics type and units when only a single statistic is selected.
|
||||||
* @type {boolean}
|
* @type {boolean}
|
||||||
@ -92,6 +99,7 @@ class HaStatisticsPicker extends LitElement {
|
|||||||
.includeStatisticsUnitOfMeasurement=${this
|
.includeStatisticsUnitOfMeasurement=${this
|
||||||
.includeStatisticsUnitOfMeasurement}
|
.includeStatisticsUnitOfMeasurement}
|
||||||
.includeUnitClass=${this.includeUnitClass}
|
.includeUnitClass=${this.includeUnitClass}
|
||||||
|
.includeDeviceClass=${this.includeDeviceClass}
|
||||||
.statisticTypes=${this.statisticTypes}
|
.statisticTypes=${this.statisticTypes}
|
||||||
.statisticIds=${this.statisticIds}
|
.statisticIds=${this.statisticIds}
|
||||||
.label=${this.pickStatisticLabel}
|
.label=${this.pickStatisticLabel}
|
||||||
|
@ -62,6 +62,7 @@ export const emptyBatteryEnergyPreference =
|
|||||||
stat_energy_from: "",
|
stat_energy_from: "",
|
||||||
stat_energy_to: "",
|
stat_energy_to: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({
|
export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({
|
||||||
type: "gas",
|
type: "gas",
|
||||||
stat_energy_from: "",
|
stat_energy_from: "",
|
||||||
@ -70,6 +71,15 @@ export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({
|
|||||||
number_energy_price: null,
|
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 {
|
interface EnergySolarForecast {
|
||||||
wh_hours: Record<string, number>;
|
wh_hours: Record<string, number>;
|
||||||
}
|
}
|
||||||
@ -130,7 +140,22 @@ export interface BatterySourceTypeEnergyPreference {
|
|||||||
export interface GasSourceTypeEnergyPreference {
|
export interface GasSourceTypeEnergyPreference {
|
||||||
type: "gas";
|
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;
|
stat_energy_from: string;
|
||||||
|
|
||||||
// $ meter
|
// $ meter
|
||||||
@ -146,7 +171,8 @@ type EnergySource =
|
|||||||
| SolarSourceTypeEnergyPreference
|
| SolarSourceTypeEnergyPreference
|
||||||
| GridSourceTypeEnergyPreference
|
| GridSourceTypeEnergyPreference
|
||||||
| BatterySourceTypeEnergyPreference
|
| BatterySourceTypeEnergyPreference
|
||||||
| GasSourceTypeEnergyPreference;
|
| GasSourceTypeEnergyPreference
|
||||||
|
| WaterSourceTypeEnergyPreference;
|
||||||
|
|
||||||
export interface EnergyPreferences {
|
export interface EnergyPreferences {
|
||||||
energy_sources: EnergySource[];
|
energy_sources: EnergySource[];
|
||||||
@ -222,6 +248,7 @@ interface EnergySourceByType {
|
|||||||
solar?: SolarSourceTypeEnergyPreference[];
|
solar?: SolarSourceTypeEnergyPreference[];
|
||||||
battery?: BatterySourceTypeEnergyPreference[];
|
battery?: BatterySourceTypeEnergyPreference[];
|
||||||
gas?: GasSourceTypeEnergyPreference[];
|
gas?: GasSourceTypeEnergyPreference[];
|
||||||
|
water?: WaterSourceTypeEnergyPreference[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const energySourcesByType = (prefs: EnergyPreferences) =>
|
export const energySourcesByType = (prefs: EnergyPreferences) =>
|
||||||
@ -255,7 +282,7 @@ export const getReferencedStatisticIds = (
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (source.type === "gas") {
|
if (source.type === "gas" || source.type === "water") {
|
||||||
statIDs.push(source.stat_energy_from);
|
statIDs.push(source.stat_energy_from);
|
||||||
if (source.stat_cost) {
|
if (source.stat_cost) {
|
||||||
statIDs.push(source.stat_cost);
|
statIDs.push(source.stat_cost);
|
||||||
@ -642,3 +669,6 @@ export const getEnergyGasUnit = (
|
|||||||
? "m³"
|
? "m³"
|
||||||
: "ft³";
|
: "ft³";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEnergyWaterUnit = (hass: HomeAssistant): string | undefined =>
|
||||||
|
hass.config.unit_system.length === "km" ? "m³" : "ft³";
|
||||||
|
204
src/panels/config/energy/components/ha-energy-water-settings.ts
Normal file
204
src/panels/config/energy/components/ha-energy-water-settings.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -107,6 +107,7 @@ export class DialogEnergyGasSettings
|
|||||||
"volume",
|
"volume",
|
||||||
"energy",
|
"energy",
|
||||||
]}
|
]}
|
||||||
|
include-device-class="gas"
|
||||||
.value=${this._source.stat_energy_from}
|
.value=${this._source.stat_energy_from}
|
||||||
.label=${`${this.hass.localize(
|
.label=${`${this.hass.localize(
|
||||||
"ui.panel.config.energy.gas.dialog.gas_usage"
|
"ui.panel.config.energy.gas.dialog.gas_usage"
|
||||||
|
281
src/panels/config/energy/dialogs/dialog-energy-water-settings.ts
Normal file
281
src/panels/config/energy/dialogs/dialog-energy-water-settings.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ import {
|
|||||||
FlowToGridSourceEnergyPreference,
|
FlowToGridSourceEnergyPreference,
|
||||||
GasSourceTypeEnergyPreference,
|
GasSourceTypeEnergyPreference,
|
||||||
SolarSourceTypeEnergyPreference,
|
SolarSourceTypeEnergyPreference,
|
||||||
|
WaterSourceTypeEnergyPreference,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
import { StatisticsMetaData } from "../../../../data/recorder";
|
import { StatisticsMetaData } from "../../../../data/recorder";
|
||||||
|
|
||||||
@ -51,6 +52,12 @@ export interface EnergySettingsGasDialogParams {
|
|||||||
saveCallback: (source: GasSourceTypeEnergyPreference) => Promise<void>;
|
saveCallback: (source: GasSourceTypeEnergyPreference) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EnergySettingsWaterDialogParams {
|
||||||
|
source?: WaterSourceTypeEnergyPreference;
|
||||||
|
metadata?: StatisticsMetaData;
|
||||||
|
saveCallback: (source: WaterSourceTypeEnergyPreference) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnergySettingsDeviceDialogParams {
|
export interface EnergySettingsDeviceDialogParams {
|
||||||
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
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 = (
|
export const showEnergySettingsGridFlowFromDialog = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
dialogParams: EnergySettingsGridFlowFromDialogParams
|
dialogParams: EnergySettingsGridFlowFromDialogParams
|
||||||
|
@ -24,6 +24,7 @@ import "./components/ha-energy-grid-settings";
|
|||||||
import "./components/ha-energy-solar-settings";
|
import "./components/ha-energy-solar-settings";
|
||||||
import "./components/ha-energy-battery-settings";
|
import "./components/ha-energy-battery-settings";
|
||||||
import "./components/ha-energy-gas-settings";
|
import "./components/ha-energy-gas-settings";
|
||||||
|
import "./components/ha-energy-water-settings";
|
||||||
|
|
||||||
const INITIAL_CONFIG: EnergyPreferences = {
|
const INITIAL_CONFIG: EnergyPreferences = {
|
||||||
energy_sources: [],
|
energy_sources: [],
|
||||||
@ -116,6 +117,13 @@ class HaConfigEnergy extends LitElement {
|
|||||||
.validationResult=${this._validationResult}
|
.validationResult=${this._validationResult}
|
||||||
@value-changed=${this._prefsChanged}
|
@value-changed=${this._prefsChanged}
|
||||||
></ha-energy-gas-settings>
|
></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
|
<ha-energy-device-settings
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.preferences=${this._preferences!}
|
.preferences=${this._preferences!}
|
||||||
|
@ -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-solar-settings";
|
||||||
import "../../config/energy/components/ha-energy-battery-settings";
|
import "../../config/energy/components/ha-energy-battery-settings";
|
||||||
import "../../config/energy/components/ha-energy-gas-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 "../../config/energy/components/ha-energy-device-settings";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
@ -54,7 +55,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
|||||||
<p>
|
<p>
|
||||||
${this.hass.localize("ui.panel.energy.setup.step", {
|
${this.hass.localize("ui.panel.energy.setup.step", {
|
||||||
step: this._step + 1,
|
step: this._step + 1,
|
||||||
steps: 5,
|
steps: 6,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
${this._step === 0
|
${this._step === 0
|
||||||
@ -82,6 +83,12 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
|||||||
.preferences=${this._preferences}
|
.preferences=${this._preferences}
|
||||||
@value-changed=${this._prefsChanged}
|
@value-changed=${this._prefsChanged}
|
||||||
></ha-energy-gas-settings>`
|
></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
|
: html`<ha-energy-device-settings
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.preferences=${this._preferences}
|
.preferences=${this._preferences}
|
||||||
@ -120,7 +127,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _next() {
|
private _next() {
|
||||||
if (this._step === 4) {
|
if (this._step === 5) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._step++;
|
this._step++;
|
||||||
|
@ -52,6 +52,10 @@ export class EnergyStrategy {
|
|||||||
);
|
);
|
||||||
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
|
||||||
|
|
||||||
|
const hasWater = prefs.energy_sources.some(
|
||||||
|
(source) => source.type === "water"
|
||||||
|
);
|
||||||
|
|
||||||
if (info.narrow) {
|
if (info.narrow) {
|
||||||
view.cards!.push({
|
view.cards!.push({
|
||||||
type: "energy-date-selection",
|
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.
|
// Only include if we have a grid.
|
||||||
if (hasGrid) {
|
if (hasGrid) {
|
||||||
view.cards!.push({
|
view.cards!.push({
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
import {
|
import {
|
||||||
mdiArrowDown,
|
mdiArrowDown,
|
||||||
mdiArrowLeft,
|
mdiArrowLeft,
|
||||||
@ -9,12 +10,12 @@ import {
|
|||||||
mdiLeaf,
|
mdiLeaf,
|
||||||
mdiSolarPower,
|
mdiSolarPower,
|
||||||
mdiTransmissionTower,
|
mdiTransmissionTower,
|
||||||
|
mdiWater,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { css, html, LitElement, svg } from "lit";
|
import { css, html, LitElement, svg } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import "@material/mwc-button";
|
|
||||||
import { formatNumber } from "../../../../common/number/format_number";
|
import { formatNumber } from "../../../../common/number/format_number";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
@ -23,6 +24,7 @@ import {
|
|||||||
energySourcesByType,
|
energySourcesByType,
|
||||||
getEnergyDataCollection,
|
getEnergyDataCollection,
|
||||||
getEnergyGasUnit,
|
getEnergyGasUnit,
|
||||||
|
getEnergyWaterUnit,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
|
import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
@ -83,6 +85,7 @@ class HuiEnergyDistrubutionCard
|
|||||||
const hasSolarProduction = types.solar !== undefined;
|
const hasSolarProduction = types.solar !== undefined;
|
||||||
const hasBattery = types.battery !== undefined;
|
const hasBattery = types.battery !== undefined;
|
||||||
const hasGas = types.gas !== undefined;
|
const hasGas = types.gas !== undefined;
|
||||||
|
const hasWater = types.water !== undefined;
|
||||||
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
||||||
|
|
||||||
const totalFromGrid =
|
const totalFromGrid =
|
||||||
@ -91,6 +94,15 @@ class HuiEnergyDistrubutionCard
|
|||||||
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
|
types.grid![0].flow_from.map((flow) => flow.stat_energy_from)
|
||||||
) ?? 0;
|
) ?? 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;
|
let gasUsage: number | null = null;
|
||||||
if (hasGas) {
|
if (hasGas) {
|
||||||
gasUsage =
|
gasUsage =
|
||||||
@ -255,7 +267,10 @@ class HuiEnergyDistrubutionCard
|
|||||||
return html`
|
return html`
|
||||||
<ha-card .header=${this._config.title}>
|
<ha-card .header=${this._config.title}>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${lowCarbonEnergy !== undefined || hasSolarProduction || hasGas
|
${lowCarbonEnergy !== undefined ||
|
||||||
|
hasSolarProduction ||
|
||||||
|
hasGas ||
|
||||||
|
hasWater
|
||||||
? html`<div class="row">
|
? html`<div class="row">
|
||||||
${lowCarbonEnergy === undefined
|
${lowCarbonEnergy === undefined
|
||||||
? html`<div class="spacer"></div>`
|
? html`<div class="spacer"></div>`
|
||||||
@ -298,7 +313,7 @@ class HuiEnergyDistrubutionCard
|
|||||||
kWh
|
kWh
|
||||||
</div>
|
</div>
|
||||||
</div>`
|
</div>`
|
||||||
: hasGas
|
: hasGas || hasWater
|
||||||
? html`<div class="spacer"></div>`
|
? html`<div class="spacer"></div>`
|
||||||
: ""}
|
: ""}
|
||||||
${hasGas
|
${hasGas
|
||||||
@ -338,6 +353,39 @@ class HuiEnergyDistrubutionCard
|
|||||||
: ""}
|
: ""}
|
||||||
</svg>
|
</svg>
|
||||||
</div>`
|
</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>`}
|
: html`<div class="spacer"></div>`}
|
||||||
</div>`
|
</div>`
|
||||||
: ""}
|
: ""}
|
||||||
@ -460,50 +508,99 @@ class HuiEnergyDistrubutionCard
|
|||||||
</svg>`
|
</svg>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
<span class="label"
|
${hasGas && hasWater
|
||||||
>${this.hass.localize(
|
? ""
|
||||||
"ui.panel.lovelace.cards.energy.energy_distribution.home"
|
: html`<span class="label"
|
||||||
)}</span
|
>${this.hass.localize(
|
||||||
>
|
"ui.panel.lovelace.cards.energy.energy_distribution.home"
|
||||||
|
)}</span
|
||||||
|
>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${hasBattery
|
${hasBattery || (hasGas && hasWater)
|
||||||
? html`<div class="row">
|
? html`<div class="row">
|
||||||
<div class="spacer"></div>
|
<div class="spacer"></div>
|
||||||
<div class="circle-container battery">
|
${hasBattery
|
||||||
<div class="circle">
|
? html` <div class="circle-container battery">
|
||||||
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
|
<div class="circle">
|
||||||
<span class="battery-in">
|
<ha-svg-icon .path=${mdiBatteryHigh}></ha-svg-icon>
|
||||||
<ha-svg-icon
|
<span class="battery-in">
|
||||||
class="small"
|
<ha-svg-icon
|
||||||
.path=${mdiArrowDown}
|
class="small"
|
||||||
></ha-svg-icon
|
.path=${mdiArrowDown}
|
||||||
>${formatNumber(totalBatteryIn || 0, this.hass.locale, {
|
></ha-svg-icon
|
||||||
maximumFractionDigits: 1,
|
>${formatNumber(
|
||||||
})}
|
totalBatteryIn || 0,
|
||||||
kWh</span
|
this.hass.locale,
|
||||||
>
|
{
|
||||||
<span class="battery-out">
|
maximumFractionDigits: 1,
|
||||||
<ha-svg-icon
|
}
|
||||||
class="small"
|
)}
|
||||||
.path=${mdiArrowUp}
|
kWh</span
|
||||||
></ha-svg-icon
|
>
|
||||||
>${formatNumber(totalBatteryOut || 0, this.hass.locale, {
|
<span class="battery-out">
|
||||||
maximumFractionDigits: 1,
|
<ha-svg-icon
|
||||||
})}
|
class="small"
|
||||||
kWh</span
|
.path=${mdiArrowUp}
|
||||||
>
|
></ha-svg-icon
|
||||||
</div>
|
>${formatNumber(
|
||||||
<span class="label"
|
totalBatteryOut || 0,
|
||||||
>${this.hass.localize(
|
this.hass.locale,
|
||||||
"ui.panel.lovelace.cards.energy.energy_distribution.battery"
|
{
|
||||||
)}</span
|
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>
|
<animateMotion
|
||||||
<div class="spacer"></div>
|
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>`
|
||||||
: ""}
|
: ""}
|
||||||
<div class="lines ${classMap({ battery: hasBattery })}">
|
<div
|
||||||
|
class="lines ${classMap({
|
||||||
|
high: hasBattery || (hasGas && hasWater),
|
||||||
|
})}"
|
||||||
|
>
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -713,7 +810,7 @@ class HuiEnergyDistrubutionCard
|
|||||||
padding: 0 16px 16px;
|
padding: 0 16px 16px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.lines.battery {
|
.lines.high {
|
||||||
bottom: 100px;
|
bottom: 100px;
|
||||||
height: 156px;
|
height: 156px;
|
||||||
}
|
}
|
||||||
@ -744,6 +841,15 @@ class HuiEnergyDistrubutionCard
|
|||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
height: 130px;
|
height: 130px;
|
||||||
}
|
}
|
||||||
|
.circle-container.water {
|
||||||
|
margin-left: 4px;
|
||||||
|
height: 130px;
|
||||||
|
}
|
||||||
|
.circle-container.water.bottom {
|
||||||
|
position: relative;
|
||||||
|
top: -20px;
|
||||||
|
margin-bottom: -20px;
|
||||||
|
}
|
||||||
.circle-container.battery {
|
.circle-container.battery {
|
||||||
height: 110px;
|
height: 110px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@ -777,6 +883,7 @@ class HuiEnergyDistrubutionCard
|
|||||||
.label {
|
.label {
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
line,
|
line,
|
||||||
path {
|
path {
|
||||||
@ -804,6 +911,17 @@ class HuiEnergyDistrubutionCard
|
|||||||
.gas .circle {
|
.gas .circle {
|
||||||
border-color: var(--energy-gas-color);
|
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 {
|
.low-carbon line {
|
||||||
stroke: var(--energy-non-fossil-color);
|
stroke: var(--energy-non-fossil-color);
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import {
|
|||||||
energySourcesByType,
|
energySourcesByType,
|
||||||
getEnergyDataCollection,
|
getEnergyDataCollection,
|
||||||
getEnergyGasUnit,
|
getEnergyGasUnit,
|
||||||
|
getEnergyWaterUnit,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
import {
|
import {
|
||||||
calculateStatisticSumGrowth,
|
calculateStatisticSumGrowth,
|
||||||
@ -83,6 +84,8 @@ export class HuiEnergySourcesTableCard
|
|||||||
let totalBattery = 0;
|
let totalBattery = 0;
|
||||||
let totalGas = 0;
|
let totalGas = 0;
|
||||||
let totalGasCost = 0;
|
let totalGasCost = 0;
|
||||||
|
let totalWater = 0;
|
||||||
|
let totalWaterCost = 0;
|
||||||
|
|
||||||
let totalGridCompare = 0;
|
let totalGridCompare = 0;
|
||||||
let totalGridCostCompare = 0;
|
let totalGridCostCompare = 0;
|
||||||
@ -90,6 +93,8 @@ export class HuiEnergySourcesTableCard
|
|||||||
let totalBatteryCompare = 0;
|
let totalBatteryCompare = 0;
|
||||||
let totalGasCompare = 0;
|
let totalGasCompare = 0;
|
||||||
let totalGasCostCompare = 0;
|
let totalGasCostCompare = 0;
|
||||||
|
let totalWaterCompare = 0;
|
||||||
|
let totalWaterCostCompare = 0;
|
||||||
|
|
||||||
const types = energySourcesByType(this._data.prefs);
|
const types = energySourcesByType(this._data.prefs);
|
||||||
|
|
||||||
@ -112,6 +117,9 @@ export class HuiEnergySourcesTableCard
|
|||||||
const gasColor = computedStyles
|
const gasColor = computedStyles
|
||||||
.getPropertyValue("--energy-gas-color")
|
.getPropertyValue("--energy-gas-color")
|
||||||
.trim();
|
.trim();
|
||||||
|
const waterColor = computedStyles
|
||||||
|
.getPropertyValue("--energy-water-color")
|
||||||
|
.trim();
|
||||||
|
|
||||||
const showCosts =
|
const showCosts =
|
||||||
types.grid?.[0].flow_from.some(
|
types.grid?.[0].flow_from.some(
|
||||||
@ -127,12 +135,18 @@ export class HuiEnergySourcesTableCard
|
|||||||
types.gas?.some(
|
types.gas?.some(
|
||||||
(flow) =>
|
(flow) =>
|
||||||
flow.stat_cost || flow.entity_energy_price || flow.number_energy_price
|
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 =
|
const gasUnit =
|
||||||
getEnergyGasUnit(this.hass, this._data.prefs, this._data.statsMetadata) ||
|
getEnergyGasUnit(this.hass, this._data.prefs, this._data.statsMetadata) ||
|
||||||
"";
|
"";
|
||||||
|
|
||||||
|
const waterUnit = getEnergyWaterUnit(this.hass) || "m³";
|
||||||
|
|
||||||
const compare = this._data.statsCompare !== undefined;
|
const compare = this._data.statsCompare !== undefined;
|
||||||
|
|
||||||
return html` <ha-card>
|
return html` <ha-card>
|
||||||
@ -851,7 +865,157 @@ export class HuiEnergySourcesTableCard
|
|||||||
: ""}
|
: ""}
|
||||||
</tr>`
|
</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">
|
? html`<tr class="mdc-data-table__row total">
|
||||||
<td class="mdc-data-table__cell"></td>
|
<td class="mdc-data-table__cell"></td>
|
||||||
<th class="mdc-data-table__cell" scope="row">
|
<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"
|
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||||
>
|
>
|
||||||
${formatNumber(
|
${formatNumber(
|
||||||
totalGasCostCompare + totalGridCostCompare,
|
totalGasCostCompare +
|
||||||
|
totalGridCostCompare +
|
||||||
|
totalWaterCostCompare,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
{
|
{
|
||||||
style: "currency",
|
style: "currency",
|
||||||
@ -881,7 +1047,7 @@ export class HuiEnergySourcesTableCard
|
|||||||
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
class="mdc-data-table__cell mdc-data-table__cell--numeric"
|
||||||
>
|
>
|
||||||
${formatNumber(
|
${formatNumber(
|
||||||
totalGasCost + totalGridCost,
|
totalGasCost + totalGridCost + totalWaterCost,
|
||||||
this.hass.locale,
|
this.hass.locale,
|
||||||
{
|
{
|
||||||
style: "currency",
|
style: "currency",
|
||||||
@ -922,6 +1088,7 @@ export class HuiEnergySourcesTableCard
|
|||||||
}
|
}
|
||||||
ha-card {
|
ha-card {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.card-header {
|
.card-header {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
431
src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts
Normal file
431
src/panels/lovelace/cards/energy/hui-energy-water-graph-card.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -133,6 +133,12 @@ export interface EnergyGasGraphCardConfig extends LovelaceCardConfig {
|
|||||||
collection_key?: string;
|
collection_key?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EnergyWaterGraphCardConfig extends LovelaceCardConfig {
|
||||||
|
type: "energy-water-graph";
|
||||||
|
title?: string;
|
||||||
|
collection_key?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
|
export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig {
|
||||||
type: "energy-devices-graph";
|
type: "energy-devices-graph";
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -47,6 +47,8 @@ const LAZY_LOAD_TYPES = {
|
|||||||
"energy-distribution": () =>
|
"energy-distribution": () =>
|
||||||
import("../cards/energy/hui-energy-distribution-card"),
|
import("../cards/energy/hui-energy-distribution-card"),
|
||||||
"energy-gas-graph": () => import("../cards/energy/hui-energy-gas-graph-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": () =>
|
"energy-grid-neutrality-gauge": () =>
|
||||||
import("../cards/energy/hui-energy-grid-neutrality-gauge-card"),
|
import("../cards/energy/hui-energy-grid-neutrality-gauge-card"),
|
||||||
"energy-solar-consumed-gauge": () =>
|
"energy-solar-consumed-gauge": () =>
|
||||||
|
@ -91,6 +91,7 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--energy-battery-out-color: #4db6ac;
|
--energy-battery-out-color: #4db6ac;
|
||||||
--energy-battery-in-color: #f06292;
|
--energy-battery-in-color: #f06292;
|
||||||
--energy-gas-color: #8E021B;
|
--energy-gas-color: #8E021B;
|
||||||
|
--energy-water-color: #00bcd4;
|
||||||
|
|
||||||
/* opacity for dark text on a light background */
|
/* opacity for dark text on a light background */
|
||||||
--dark-divider-opacity: 0.12;
|
--dark-divider-opacity: 0.12;
|
||||||
|
@ -1500,6 +1500,29 @@
|
|||||||
"m3_or_kWh": "ft³, m³, Wh, kWh, MWh or GJ"
|
"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": {
|
"device_consumption": {
|
||||||
"title": "Individual devices",
|
"title": "Individual devices",
|
||||||
"sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",
|
"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",
|
"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:"
|
"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": {
|
"entity_unexpected_unit_energy_price": {
|
||||||
"title": "Unexpected unit of measurement",
|
"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'':"
|
"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",
|
"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³'':"
|
"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": {
|
"entity_unexpected_state_class": {
|
||||||
"title": "Unexpected state class",
|
"title": "Unexpected state class",
|
||||||
"description": "The following entities do not have the expected state class:"
|
"description": "The following entities do not have the expected state class:"
|
||||||
@ -3681,6 +3712,7 @@
|
|||||||
"energy_sources_table": {
|
"energy_sources_table": {
|
||||||
"grid_total": "Grid total",
|
"grid_total": "Grid total",
|
||||||
"gas_total": "Gas total",
|
"gas_total": "Gas total",
|
||||||
|
"water_total": "Water total",
|
||||||
"source": "Source",
|
"source": "Source",
|
||||||
"energy": "Energy",
|
"energy": "Energy",
|
||||||
"cost": "Cost",
|
"cost": "Cost",
|
||||||
@ -3711,6 +3743,7 @@
|
|||||||
"title_today": "Energy distribution today",
|
"title_today": "Energy distribution today",
|
||||||
"grid": "Grid",
|
"grid": "Grid",
|
||||||
"gas": "Gas",
|
"gas": "Gas",
|
||||||
|
"water": "Water",
|
||||||
"solar": "Solar",
|
"solar": "Solar",
|
||||||
"low_carbon": "Low-carbon",
|
"low_carbon": "Low-carbon",
|
||||||
"home": "Home",
|
"home": "Home",
|
||||||
@ -4773,6 +4806,7 @@
|
|||||||
"energy_usage_graph_title": "Energy usage",
|
"energy_usage_graph_title": "Energy usage",
|
||||||
"energy_solar_graph_title": "Solar production",
|
"energy_solar_graph_title": "Solar production",
|
||||||
"energy_gas_graph_title": "Gas consumption",
|
"energy_gas_graph_title": "Gas consumption",
|
||||||
|
"energy_water_graph_title": "Water consumption",
|
||||||
"energy_distribution_title": "Energy distribution",
|
"energy_distribution_title": "Energy distribution",
|
||||||
"energy_sources_table_title": "Sources",
|
"energy_sources_table_title": "Sources",
|
||||||
"energy_devices_graph_title": "Monitor individual devices"
|
"energy_devices_graph_title": "Monitor individual devices"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user