mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-17 15:00:31 +00:00
Compare commits
2 Commits
copilot/fi
...
water-sank
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00d75d44b3 | ||
|
|
c472010ac5 |
@@ -84,6 +84,7 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
|
|||||||
stat_consumption: "sensor.energy_boiler",
|
stat_consumption: "sensor.energy_boiler",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
device_consumption_water: [],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
hass.mockWS(
|
hass.mockWS(
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export type EnergySource =
|
|||||||
export interface EnergyPreferences {
|
export interface EnergyPreferences {
|
||||||
energy_sources: EnergySource[];
|
energy_sources: EnergySource[];
|
||||||
device_consumption: DeviceConsumptionEnergyPreference[];
|
device_consumption: DeviceConsumptionEnergyPreference[];
|
||||||
|
device_consumption_water: DeviceConsumptionEnergyPreference[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EnergyInfo {
|
export interface EnergyInfo {
|
||||||
@@ -347,6 +348,11 @@ export const getReferencedStatisticIds = (
|
|||||||
if (!(includeTypes && !includeTypes.includes("device"))) {
|
if (!(includeTypes && !includeTypes.includes("device"))) {
|
||||||
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
|
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
|
||||||
}
|
}
|
||||||
|
if (!(includeTypes && !includeTypes.includes("water"))) {
|
||||||
|
statIDs.push(
|
||||||
|
...prefs.device_consumption_water.map((d) => d.stat_consumption)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return statIDs;
|
return statIDs;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
import {
|
||||||
|
mdiDelete,
|
||||||
|
mdiWater,
|
||||||
|
mdiDragHorizontalVariant,
|
||||||
|
mdiPencil,
|
||||||
|
mdiPlus,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-icon-button";
|
||||||
|
import "../../../../components/ha-sortable";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import type {
|
||||||
|
DeviceConsumptionEnergyPreference,
|
||||||
|
EnergyPreferences,
|
||||||
|
EnergyPreferencesValidation,
|
||||||
|
} from "../../../../data/energy";
|
||||||
|
import { saveEnergyPreferences } from "../../../../data/energy";
|
||||||
|
import type { StatisticsMetaData } from "../../../../data/recorder";
|
||||||
|
import { getStatisticLabel } from "../../../../data/recorder";
|
||||||
|
import {
|
||||||
|
showAlertDialog,
|
||||||
|
showConfirmationDialog,
|
||||||
|
} from "../../../../dialogs/generic/show-dialog-box";
|
||||||
|
import { haStyle } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import { documentationUrl } from "../../../../util/documentation-url";
|
||||||
|
import { showEnergySettingsDeviceWaterDialog } from "../dialogs/show-dialogs-energy";
|
||||||
|
import "./ha-energy-validation-result";
|
||||||
|
import { energyCardStyles } from "./styles";
|
||||||
|
|
||||||
|
@customElement("ha-energy-device-settings-water")
|
||||||
|
export class EnergyDeviceSettingsWater 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 {
|
||||||
|
return html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<h1 class="card-header">
|
||||||
|
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.title"
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<p>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.sub"
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href=${documentationUrl(
|
||||||
|
this.hass,
|
||||||
|
"/docs/energy/water/#individual-devices"
|
||||||
|
)}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.learn_more"
|
||||||
|
)}</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<h3>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.devices"
|
||||||
|
)}
|
||||||
|
</h3>
|
||||||
|
<ha-sortable handle-selector=".handle" @item-moved=${this._itemMoved}>
|
||||||
|
<div class="devices">
|
||||||
|
${repeat(
|
||||||
|
this.preferences.device_consumption_water,
|
||||||
|
(device) => device.stat_consumption,
|
||||||
|
(device) => html`
|
||||||
|
<div class="row" .device=${device}>
|
||||||
|
<div class="handle">
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${mdiDragHorizontalVariant}
|
||||||
|
></ha-svg-icon>
|
||||||
|
</div>
|
||||||
|
<span class="content"
|
||||||
|
>${device.name ||
|
||||||
|
getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
device.stat_consumption,
|
||||||
|
this.statsMetadata?.[device.stat_consumption]
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("ui.common.edit")}
|
||||||
|
@click=${this._editDevice}
|
||||||
|
.path=${mdiPencil}
|
||||||
|
></ha-icon-button>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("ui.common.delete")}
|
||||||
|
@click=${this._deleteDevice}
|
||||||
|
.device=${device}
|
||||||
|
.path=${mdiDelete}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</ha-sortable>
|
||||||
|
<div class="row">
|
||||||
|
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._addDevice}
|
||||||
|
appearance="filled"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.add_device"
|
||||||
|
)}</ha-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _itemMoved(ev: CustomEvent): void {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const { oldIndex, newIndex } = ev.detail;
|
||||||
|
const devices = this.preferences.device_consumption_water.concat();
|
||||||
|
const device = devices.splice(oldIndex, 1)[0];
|
||||||
|
devices.splice(newIndex, 0, device);
|
||||||
|
|
||||||
|
const newPrefs = {
|
||||||
|
...this.preferences,
|
||||||
|
device_consumption_water: devices,
|
||||||
|
};
|
||||||
|
fireEvent(this, "value-changed", { value: newPrefs });
|
||||||
|
this._savePreferences(newPrefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _editDevice(ev) {
|
||||||
|
const origDevice: DeviceConsumptionEnergyPreference =
|
||||||
|
ev.currentTarget.closest(".row").device;
|
||||||
|
showEnergySettingsDeviceWaterDialog(this, {
|
||||||
|
statsMetadata: this.statsMetadata,
|
||||||
|
device: { ...origDevice },
|
||||||
|
device_consumptions: this.preferences
|
||||||
|
.device_consumption_water as DeviceConsumptionEnergyPreference[],
|
||||||
|
saveCallback: async (newDevice) => {
|
||||||
|
const newPrefs = {
|
||||||
|
...this.preferences,
|
||||||
|
device_consumption_water:
|
||||||
|
this.preferences.device_consumption_water.map((d) =>
|
||||||
|
d === origDevice ? newDevice : d
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this._sanitizeParents(newPrefs);
|
||||||
|
await this._savePreferences(newPrefs);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addDevice() {
|
||||||
|
showEnergySettingsDeviceWaterDialog(this, {
|
||||||
|
statsMetadata: this.statsMetadata,
|
||||||
|
device_consumptions: this.preferences
|
||||||
|
.device_consumption_water as DeviceConsumptionEnergyPreference[],
|
||||||
|
saveCallback: async (device) => {
|
||||||
|
await this._savePreferences({
|
||||||
|
...this.preferences,
|
||||||
|
device_consumption_water:
|
||||||
|
this.preferences.device_consumption_water.concat(device),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _sanitizeParents(prefs: EnergyPreferences) {
|
||||||
|
const statIds = prefs.device_consumption_water.map(
|
||||||
|
(d) => d.stat_consumption
|
||||||
|
);
|
||||||
|
prefs.device_consumption_water.forEach((d) => {
|
||||||
|
if (d.included_in_stat && !statIds.includes(d.included_in_stat)) {
|
||||||
|
delete d.included_in_stat;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _deleteDevice(ev) {
|
||||||
|
const deviceToDelete: DeviceConsumptionEnergyPreference =
|
||||||
|
ev.currentTarget.device;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize("ui.panel.config.energy.delete_source"),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPrefs = {
|
||||||
|
...this.preferences,
|
||||||
|
device_consumption_water:
|
||||||
|
this.preferences.device_consumption_water.filter(
|
||||||
|
(device) => device !== deviceToDelete
|
||||||
|
),
|
||||||
|
};
|
||||||
|
this._sanitizeParents(newPrefs);
|
||||||
|
await this._savePreferences(newPrefs);
|
||||||
|
} 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,
|
||||||
|
css`
|
||||||
|
.handle {
|
||||||
|
cursor: move; /* fallback if grab cursor is unsupported */
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-energy-device-settings-water": EnergyDeviceSettingsWater;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
import { mdiWater } from "@mdi/js";
|
||||||
|
import type { CSSResultGroup } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
|
import "../../../../components/entity/ha-entity-picker";
|
||||||
|
import "../../../../components/entity/ha-statistic-picker";
|
||||||
|
import "../../../../components/ha-dialog";
|
||||||
|
import "../../../../components/ha-radio";
|
||||||
|
import "../../../../components/ha-button";
|
||||||
|
import "../../../../components/ha-select";
|
||||||
|
import "../../../../components/ha-list-item";
|
||||||
|
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
|
||||||
|
import { energyStatisticHelpUrl } from "../../../../data/energy";
|
||||||
|
import { getStatisticLabel } from "../../../../data/recorder";
|
||||||
|
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
|
||||||
|
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
|
||||||
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { EnergySettingsDeviceWaterDialogParams } from "./show-dialogs-energy";
|
||||||
|
|
||||||
|
const volumeUnitClasses = ["volume"];
|
||||||
|
|
||||||
|
@customElement("dialog-energy-device-settings-water")
|
||||||
|
export class DialogEnergyDeviceSettingsWater
|
||||||
|
extends LitElement
|
||||||
|
implements HassDialog<EnergySettingsDeviceWaterDialogParams>
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _params?: EnergySettingsDeviceWaterDialogParams;
|
||||||
|
|
||||||
|
@state() private _device?: DeviceConsumptionEnergyPreference;
|
||||||
|
|
||||||
|
@state() private _volume_units?: string[];
|
||||||
|
|
||||||
|
@state() private _error?: string;
|
||||||
|
|
||||||
|
private _excludeList?: string[];
|
||||||
|
|
||||||
|
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
|
||||||
|
|
||||||
|
public async showDialog(
|
||||||
|
params: EnergySettingsDeviceWaterDialogParams
|
||||||
|
): Promise<void> {
|
||||||
|
this._params = params;
|
||||||
|
this._device = this._params.device;
|
||||||
|
this._computePossibleParents();
|
||||||
|
this._volume_units = (
|
||||||
|
await getSensorDeviceClassConvertibleUnits(this.hass, "water")
|
||||||
|
).units;
|
||||||
|
this._excludeList = this._params.device_consumptions
|
||||||
|
.map((entry) => entry.stat_consumption)
|
||||||
|
.filter((id) => id !== this._device?.stat_consumption);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _computePossibleParents() {
|
||||||
|
if (!this._device || !this._params) {
|
||||||
|
this._possibleParents = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const children: string[] = [];
|
||||||
|
const devices = this._params.device_consumptions;
|
||||||
|
function getChildren(stat) {
|
||||||
|
devices.forEach((d) => {
|
||||||
|
if (d.included_in_stat === stat) {
|
||||||
|
children.push(d.stat_consumption);
|
||||||
|
getChildren(d.stat_consumption);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
getChildren(this._device.stat_consumption);
|
||||||
|
this._possibleParents = this._params.device_consumptions.filter(
|
||||||
|
(d) =>
|
||||||
|
d.stat_consumption !== this._device!.stat_consumption &&
|
||||||
|
d.stat_consumption !== this._params?.device?.stat_consumption &&
|
||||||
|
!children.includes(d.stat_consumption)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog() {
|
||||||
|
this._params = undefined;
|
||||||
|
this._device = undefined;
|
||||||
|
this._error = undefined;
|
||||||
|
this._excludeList = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pickableUnit = this._volume_units?.join(", ") || "";
|
||||||
|
|
||||||
|
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.device_consumption_water.dialog.header"
|
||||||
|
)}`}
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
>
|
||||||
|
${this._error ? html`<p class="error">${this._error}</p>` : ""}
|
||||||
|
<div>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.dialog.selected_stat_intro",
|
||||||
|
{ unit: pickableUnit }
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ha-statistic-picker
|
||||||
|
.hass=${this.hass}
|
||||||
|
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||||
|
.includeUnitClass=${volumeUnitClasses}
|
||||||
|
.value=${this._device?.stat_consumption}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.dialog.device_consumption_water"
|
||||||
|
)}
|
||||||
|
.excludeStatistics=${this._excludeList}
|
||||||
|
@value-changed=${this._statisticChanged}
|
||||||
|
dialogInitialFocus
|
||||||
|
></ha-statistic-picker>
|
||||||
|
|
||||||
|
<ha-textfield
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.dialog.display_name"
|
||||||
|
)}
|
||||||
|
type="text"
|
||||||
|
.disabled=${!this._device}
|
||||||
|
.value=${this._device?.name || ""}
|
||||||
|
.placeholder=${this._device
|
||||||
|
? getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
this._device.stat_consumption,
|
||||||
|
this._params?.statsMetadata?.[this._device.stat_consumption]
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
@input=${this._nameChanged}
|
||||||
|
>
|
||||||
|
</ha-textfield>
|
||||||
|
|
||||||
|
<ha-select
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device"
|
||||||
|
)}
|
||||||
|
.value=${this._device?.included_in_stat || ""}
|
||||||
|
.helper=${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device_helper"
|
||||||
|
)}
|
||||||
|
.disabled=${!this._device}
|
||||||
|
@selected=${this._parentSelected}
|
||||||
|
@closed=${stopPropagation}
|
||||||
|
fixedMenuPosition
|
||||||
|
naturalMenuWidth
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
${!this._possibleParents.length
|
||||||
|
? html`
|
||||||
|
<ha-list-item disabled value="-"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
|
||||||
|
)}</ha-list-item
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: this._possibleParents.map(
|
||||||
|
(stat) => html`
|
||||||
|
<ha-list-item .value=${stat.stat_consumption}
|
||||||
|
>${stat.name ||
|
||||||
|
getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
stat.stat_consumption,
|
||||||
|
this._params?.statsMetadata?.[stat.stat_consumption]
|
||||||
|
)}</ha-list-item
|
||||||
|
>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</ha-select>
|
||||||
|
|
||||||
|
<ha-button
|
||||||
|
appearance="plain"
|
||||||
|
@click=${this.closeDialog}
|
||||||
|
slot="primaryAction"
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.common.cancel")}
|
||||||
|
</ha-button>
|
||||||
|
<ha-button
|
||||||
|
@click=${this._save}
|
||||||
|
.disabled=${!this._device}
|
||||||
|
slot="primaryAction"
|
||||||
|
>
|
||||||
|
${this.hass.localize("ui.common.save")}
|
||||||
|
</ha-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
|
||||||
|
if (!ev.detail.value) {
|
||||||
|
this._device = undefined;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._device = { stat_consumption: ev.detail.value };
|
||||||
|
this._computePossibleParents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _nameChanged(ev) {
|
||||||
|
const newDevice = {
|
||||||
|
...this._device!,
|
||||||
|
name: ev.target!.value,
|
||||||
|
} as DeviceConsumptionEnergyPreference;
|
||||||
|
if (!newDevice.name) {
|
||||||
|
delete newDevice.name;
|
||||||
|
}
|
||||||
|
this._device = newDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _parentSelected(ev) {
|
||||||
|
const newDevice = {
|
||||||
|
...this._device!,
|
||||||
|
included_in_stat: ev.target!.value,
|
||||||
|
} as DeviceConsumptionEnergyPreference;
|
||||||
|
if (!newDevice.included_in_stat) {
|
||||||
|
delete newDevice.included_in_stat;
|
||||||
|
}
|
||||||
|
this._device = newDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _save() {
|
||||||
|
try {
|
||||||
|
await this._params!.saveCallback(this._device!);
|
||||||
|
this.closeDialog();
|
||||||
|
} catch (err: any) {
|
||||||
|
this._error = err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-statistic-picker {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-select {
|
||||||
|
margin-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
margin-top: 16px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-energy-device-settings-water": DialogEnergyDeviceSettingsWater;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,6 +76,13 @@ export interface EnergySettingsDeviceDialogParams {
|
|||||||
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EnergySettingsDeviceWaterDialogParams {
|
||||||
|
device?: DeviceConsumptionEnergyPreference;
|
||||||
|
device_consumptions: DeviceConsumptionEnergyPreference[];
|
||||||
|
statsMetadata?: Record<string, StatisticsMetaData>;
|
||||||
|
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export const showEnergySettingsDeviceDialog = (
|
export const showEnergySettingsDeviceDialog = (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
dialogParams: EnergySettingsDeviceDialogParams
|
dialogParams: EnergySettingsDeviceDialogParams
|
||||||
@@ -152,3 +159,14 @@ export const showEnergySettingsGridFlowToDialog = (
|
|||||||
dialogParams: { ...dialogParams, direction: "to" },
|
dialogParams: { ...dialogParams, direction: "to" },
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const showEnergySettingsDeviceWaterDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: EnergySettingsDeviceWaterDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-energy-device-settings-water",
|
||||||
|
dialogImport: () => import("./dialog-energy-device-settings-water"),
|
||||||
|
dialogParams: dialogParams,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { haStyle } from "../../../resources/styles";
|
|||||||
import type { HomeAssistant, Route } from "../../../types";
|
import type { HomeAssistant, Route } from "../../../types";
|
||||||
import "../../../components/ha-alert";
|
import "../../../components/ha-alert";
|
||||||
import "./components/ha-energy-device-settings";
|
import "./components/ha-energy-device-settings";
|
||||||
|
import "./components/ha-energy-device-settings-water";
|
||||||
import "./components/ha-energy-grid-settings";
|
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";
|
||||||
@@ -32,6 +33,7 @@ import { fileDownload } from "../../../util/file_download";
|
|||||||
const INITIAL_CONFIG: EnergyPreferences = {
|
const INITIAL_CONFIG: EnergyPreferences = {
|
||||||
energy_sources: [],
|
energy_sources: [],
|
||||||
device_consumption: [],
|
device_consumption: [],
|
||||||
|
device_consumption_water: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@customElement("ha-config-energy")
|
@customElement("ha-config-energy")
|
||||||
@@ -142,6 +144,13 @@ class HaConfigEnergy extends LitElement {
|
|||||||
.validationResult=${this._validationResult}
|
.validationResult=${this._validationResult}
|
||||||
@value-changed=${this._prefsChanged}
|
@value-changed=${this._prefsChanged}
|
||||||
></ha-energy-device-settings>
|
></ha-energy-device-settings>
|
||||||
|
<ha-energy-device-settings-water
|
||||||
|
.hass=${this.hass}
|
||||||
|
.preferences=${this._preferences!}
|
||||||
|
.statsMetadata=${this._statsMetadata}
|
||||||
|
.validationResult=${this._validationResult}
|
||||||
|
@value-changed=${this._prefsChanged}
|
||||||
|
></ha-energy-device-settings-water>
|
||||||
</div>
|
</div>
|
||||||
</hass-subpage>
|
</hass-subpage>
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
|
|||||||
@state() private _preferences: EnergyPreferences = {
|
@state() private _preferences: EnergyPreferences = {
|
||||||
energy_sources: [],
|
energy_sources: [],
|
||||||
device_consumption: [],
|
device_consumption: [],
|
||||||
|
device_consumption_water: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
public getCardSize() {
|
public getCardSize() {
|
||||||
|
|||||||
@@ -230,6 +230,14 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
|
|||||||
group_by_area?: boolean;
|
group_by_area?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WaterSankeyCardConfig extends EnergyCardBaseConfig {
|
||||||
|
type: "water-sankey";
|
||||||
|
title?: string;
|
||||||
|
layout?: "vertical" | "horizontal" | "auto";
|
||||||
|
group_by_floor?: boolean;
|
||||||
|
group_by_area?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
export interface EntityFilterCardConfig extends LovelaceCardConfig {
|
||||||
type: "entity-filter";
|
type: "entity-filter";
|
||||||
entities: (EntityFilterEntityConfig | string)[];
|
entities: (EntityFilterEntityConfig | string)[];
|
||||||
|
|||||||
453
src/panels/lovelace/cards/water/hui-water-sankey-card.ts
Normal file
453
src/panels/lovelace/cards/water/hui-water-sankey-card.ts
Normal file
@@ -0,0 +1,453 @@
|
|||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
|
import type { PropertyValues } from "lit";
|
||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-svg-icon";
|
||||||
|
import type { EnergyData } from "../../../../data/energy";
|
||||||
|
import { getEnergyDataCollection } from "../../../../data/energy";
|
||||||
|
import {
|
||||||
|
calculateStatisticSumGrowth,
|
||||||
|
getStatisticLabel,
|
||||||
|
} from "../../../../data/recorder";
|
||||||
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
|
import type { HomeAssistant } from "../../../../types";
|
||||||
|
import type { LovelaceCard, LovelaceGridOptions } from "../../types";
|
||||||
|
import type { WaterSankeyCardConfig } from "../types";
|
||||||
|
import "../../../../components/chart/ha-sankey-chart";
|
||||||
|
import type { Link, Node } from "../../../../components/chart/ha-sankey-chart";
|
||||||
|
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||||
|
import { formatNumber } from "../../../../common/number/format_number";
|
||||||
|
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||||
|
import { MobileAwareMixin } from "../../../../mixins/mobile-aware-mixin";
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG: Partial<WaterSankeyCardConfig> = {
|
||||||
|
group_by_floor: true,
|
||||||
|
group_by_area: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
@customElement("hui-water-sankey-card")
|
||||||
|
class HuiWaterSankeyCard
|
||||||
|
extends SubscribeMixin(MobileAwareMixin(LitElement))
|
||||||
|
implements LovelaceCard
|
||||||
|
{
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _config?: WaterSankeyCardConfig;
|
||||||
|
|
||||||
|
@state() private _data?: EnergyData;
|
||||||
|
|
||||||
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
|
|
||||||
|
public setConfig(config: WaterSankeyCardConfig): void {
|
||||||
|
this._config = { ...DEFAULT_CONFIG, ...config };
|
||||||
|
}
|
||||||
|
|
||||||
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
|
return [
|
||||||
|
getEnergyDataCollection(this.hass, {
|
||||||
|
key: this._config?.collection_key,
|
||||||
|
}).subscribe((data) => {
|
||||||
|
this._data = data;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardSize(): Promise<number> | number {
|
||||||
|
return 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
getGridOptions(): LovelaceGridOptions {
|
||||||
|
return {
|
||||||
|
columns: 12,
|
||||||
|
min_columns: 6,
|
||||||
|
rows: 6,
|
||||||
|
min_rows: 2,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
|
return (
|
||||||
|
changedProps.has("_config") ||
|
||||||
|
changedProps.has("_data") ||
|
||||||
|
changedProps.has("_isMobileSize")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._config) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._data) {
|
||||||
|
return html`${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.loading"
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefs = this._data.prefs;
|
||||||
|
const waterSources = prefs.energy_sources.filter(
|
||||||
|
(source) => source.type === "water"
|
||||||
|
);
|
||||||
|
|
||||||
|
const computedStyle = getComputedStyle(this);
|
||||||
|
|
||||||
|
const nodes: Node[] = [];
|
||||||
|
const links: Link[] = [];
|
||||||
|
|
||||||
|
// Calculate total water consumption from all devices
|
||||||
|
let totalWaterConsumption = 0;
|
||||||
|
prefs.device_consumption_water.forEach((device) => {
|
||||||
|
const value =
|
||||||
|
device.stat_consumption in this._data!.stats
|
||||||
|
? calculateStatisticSumGrowth(
|
||||||
|
this._data!.stats[device.stat_consumption]
|
||||||
|
) || 0
|
||||||
|
: 0;
|
||||||
|
totalWaterConsumption += value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create home/consumption node
|
||||||
|
const homeNode: Node = {
|
||||||
|
id: "home",
|
||||||
|
label: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.energy_distribution.home"
|
||||||
|
),
|
||||||
|
value: Math.max(0, totalWaterConsumption),
|
||||||
|
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||||
|
index: 1,
|
||||||
|
};
|
||||||
|
nodes.push(homeNode);
|
||||||
|
|
||||||
|
// Add water source nodes
|
||||||
|
const waterColor = computedStyle
|
||||||
|
.getPropertyValue("--energy-water-color")
|
||||||
|
.trim();
|
||||||
|
waterSources.forEach((source) => {
|
||||||
|
if (source.type !== "water") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const value =
|
||||||
|
source.stat_energy_from in this._data!.stats
|
||||||
|
? calculateStatisticSumGrowth(
|
||||||
|
this._data!.stats[source.stat_energy_from]
|
||||||
|
) || 0
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (value < 0.01) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
id: source.stat_energy_from,
|
||||||
|
label: getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
source.stat_energy_from,
|
||||||
|
this._data!.statsMetadata[source.stat_energy_from]
|
||||||
|
),
|
||||||
|
value,
|
||||||
|
color: waterColor,
|
||||||
|
index: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
links.push({
|
||||||
|
source: source.stat_energy_from,
|
||||||
|
target: "home",
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let untrackedConsumption = homeNode.value;
|
||||||
|
const deviceNodes: Node[] = [];
|
||||||
|
const parentLinks: Record<string, string> = {};
|
||||||
|
prefs.device_consumption_water.forEach((device, idx) => {
|
||||||
|
const value =
|
||||||
|
device.stat_consumption in this._data!.stats
|
||||||
|
? calculateStatisticSumGrowth(
|
||||||
|
this._data!.stats[device.stat_consumption]
|
||||||
|
) || 0
|
||||||
|
: 0;
|
||||||
|
if (value < 0.01) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const node = {
|
||||||
|
id: device.stat_consumption,
|
||||||
|
label:
|
||||||
|
device.name ||
|
||||||
|
getStatisticLabel(
|
||||||
|
this.hass,
|
||||||
|
device.stat_consumption,
|
||||||
|
this._data!.statsMetadata[device.stat_consumption]
|
||||||
|
),
|
||||||
|
value,
|
||||||
|
color: getGraphColorByIndex(idx, computedStyle),
|
||||||
|
index: 4,
|
||||||
|
parent: device.included_in_stat,
|
||||||
|
};
|
||||||
|
if (node.parent) {
|
||||||
|
parentLinks[node.id] = node.parent;
|
||||||
|
links.push({
|
||||||
|
source: node.parent,
|
||||||
|
target: node.id,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
untrackedConsumption -= value;
|
||||||
|
}
|
||||||
|
deviceNodes.push(node);
|
||||||
|
});
|
||||||
|
const devicesWithoutParent = deviceNodes.filter(
|
||||||
|
(node) => !parentLinks[node.id]
|
||||||
|
);
|
||||||
|
|
||||||
|
const { group_by_area, group_by_floor } = this._config;
|
||||||
|
if (group_by_area || group_by_floor) {
|
||||||
|
const { areas, floors } = this._groupByFloorAndArea(devicesWithoutParent);
|
||||||
|
|
||||||
|
Object.keys(floors)
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(this.hass.floors[b]?.level ?? -Infinity) -
|
||||||
|
(this.hass.floors[a]?.level ?? -Infinity)
|
||||||
|
)
|
||||||
|
.forEach((floorId) => {
|
||||||
|
let floorNodeId = `floor_${floorId}`;
|
||||||
|
if (floorId === "no_floor" || !group_by_floor) {
|
||||||
|
// link "no_floor" areas to home
|
||||||
|
floorNodeId = "home";
|
||||||
|
} else {
|
||||||
|
nodes.push({
|
||||||
|
id: floorNodeId,
|
||||||
|
label: this.hass.floors[floorId].name,
|
||||||
|
value: floors[floorId].value,
|
||||||
|
index: 2,
|
||||||
|
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||||
|
});
|
||||||
|
links.push({
|
||||||
|
source: "home",
|
||||||
|
target: floorNodeId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
floors[floorId].areas.forEach((areaId) => {
|
||||||
|
let targetNodeId: string;
|
||||||
|
|
||||||
|
if (areaId === "no_area" || !group_by_area) {
|
||||||
|
// If group_by_area is false, link devices to floor or home
|
||||||
|
targetNodeId = floorNodeId;
|
||||||
|
} else {
|
||||||
|
// Create area node and link it to floor
|
||||||
|
const areaNodeId = `area_${areaId}`;
|
||||||
|
nodes.push({
|
||||||
|
id: areaNodeId,
|
||||||
|
label: this.hass.areas[areaId]!.name,
|
||||||
|
value: areas[areaId].value,
|
||||||
|
index: 3,
|
||||||
|
color: computedStyle.getPropertyValue("--primary-color").trim(),
|
||||||
|
});
|
||||||
|
links.push({
|
||||||
|
source: floorNodeId,
|
||||||
|
target: areaNodeId,
|
||||||
|
value: areas[areaId].value,
|
||||||
|
});
|
||||||
|
targetNodeId = areaNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link devices to the appropriate target (area, floor, or home)
|
||||||
|
areas[areaId].devices.forEach((device) => {
|
||||||
|
links.push({
|
||||||
|
source: targetNodeId,
|
||||||
|
target: device.id,
|
||||||
|
value: device.value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
devicesWithoutParent.forEach((deviceNode) => {
|
||||||
|
links.push({
|
||||||
|
source: "home",
|
||||||
|
target: deviceNode.id,
|
||||||
|
value: deviceNode.value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const deviceSections = this._getDeviceSections(parentLinks, deviceNodes);
|
||||||
|
deviceSections.forEach((section, index) => {
|
||||||
|
section.forEach((node: Node) => {
|
||||||
|
nodes.push({ ...node, index: 4 + index });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// untracked consumption
|
||||||
|
if (untrackedConsumption > 0) {
|
||||||
|
nodes.push({
|
||||||
|
id: "untracked",
|
||||||
|
label: this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.energy_devices_detail_graph.untracked_consumption"
|
||||||
|
),
|
||||||
|
value: untrackedConsumption,
|
||||||
|
color: computedStyle
|
||||||
|
.getPropertyValue("--state-unavailable-color")
|
||||||
|
.trim(),
|
||||||
|
index: 3 + deviceSections.length,
|
||||||
|
});
|
||||||
|
links.push({
|
||||||
|
source: "home",
|
||||||
|
target: "untracked",
|
||||||
|
value: untrackedConsumption,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasData = nodes.some((node) => node.value > 0);
|
||||||
|
|
||||||
|
const vertical =
|
||||||
|
this._config.layout === "vertical" ||
|
||||||
|
(this._config.layout !== "horizontal" && this._isMobileSize);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card .header=${this._config.title}>
|
||||||
|
<div class="card-content">
|
||||||
|
${hasData
|
||||||
|
? html`<ha-sankey-chart
|
||||||
|
.data=${{ nodes, links }}
|
||||||
|
.vertical=${vertical}
|
||||||
|
.valueFormatter=${this._valueFormatter}
|
||||||
|
></ha-sankey-chart>`
|
||||||
|
: html`${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.cards.energy.no_data_period"
|
||||||
|
)}`}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _valueFormatter = (value: number) =>
|
||||||
|
`${formatNumber(value, this.hass.locale, value < 0.1 ? { maximumFractionDigits: 3 } : undefined)} ${this._data!.waterUnit}`;
|
||||||
|
|
||||||
|
protected _groupByFloorAndArea(deviceNodes: Node[]) {
|
||||||
|
const areas: Record<string, { value: number; devices: Node[] }> = {
|
||||||
|
no_area: {
|
||||||
|
value: 0,
|
||||||
|
devices: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const floors: Record<string, { value: number; areas: string[] }> = {
|
||||||
|
no_floor: {
|
||||||
|
value: 0,
|
||||||
|
areas: ["no_area"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
deviceNodes.forEach((deviceNode) => {
|
||||||
|
const entity = this.hass.states[deviceNode.id];
|
||||||
|
const { area, floor } = entity
|
||||||
|
? getEntityContext(
|
||||||
|
entity,
|
||||||
|
this.hass.entities,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.areas,
|
||||||
|
this.hass.floors
|
||||||
|
)
|
||||||
|
: { area: null, floor: null };
|
||||||
|
if (area) {
|
||||||
|
if (area.area_id in areas) {
|
||||||
|
areas[area.area_id].value += deviceNode.value;
|
||||||
|
areas[area.area_id].devices.push(deviceNode);
|
||||||
|
} else {
|
||||||
|
areas[area.area_id] = {
|
||||||
|
value: deviceNode.value,
|
||||||
|
devices: [deviceNode],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// see if the area has a floor
|
||||||
|
if (floor) {
|
||||||
|
if (floor.floor_id in floors) {
|
||||||
|
floors[floor.floor_id].value += deviceNode.value;
|
||||||
|
if (!floors[floor.floor_id].areas.includes(area.area_id)) {
|
||||||
|
floors[floor.floor_id].areas.push(area.area_id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
floors[floor.floor_id] = {
|
||||||
|
value: deviceNode.value,
|
||||||
|
areas: [area.area_id],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
floors.no_floor.value += deviceNode.value;
|
||||||
|
if (!floors.no_floor.areas.includes(area.area_id)) {
|
||||||
|
floors.no_floor.areas.unshift(area.area_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
areas.no_area.value += deviceNode.value;
|
||||||
|
areas.no_area.devices.push(deviceNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { areas, floors };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organizes device nodes into hierarchical sections based on parent-child relationships.
|
||||||
|
*/
|
||||||
|
protected _getDeviceSections(
|
||||||
|
parentLinks: Record<string, string>,
|
||||||
|
deviceNodes: Node[]
|
||||||
|
): Node[][] {
|
||||||
|
const parentSection: Node[] = [];
|
||||||
|
const childSection: Node[] = [];
|
||||||
|
const parentIds = Object.values(parentLinks);
|
||||||
|
const remainingLinks: typeof parentLinks = {};
|
||||||
|
|
||||||
|
deviceNodes.forEach((deviceNode) => {
|
||||||
|
const isChild = deviceNode.id in parentLinks;
|
||||||
|
const isParent = parentIds.includes(deviceNode.id);
|
||||||
|
if (isParent && !isChild) {
|
||||||
|
// Top-level parents (have children but no parents themselves)
|
||||||
|
parentSection.push(deviceNode);
|
||||||
|
} else {
|
||||||
|
childSection.push(deviceNode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter out links where parent is already in current parent section
|
||||||
|
Object.entries(parentLinks).forEach(([child, parent]) => {
|
||||||
|
if (!parentSection.some((node) => node.id === parent)) {
|
||||||
|
remainingLinks[child] = parent;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (parentSection.length > 0) {
|
||||||
|
// Recursively process child section with remaining links
|
||||||
|
return [
|
||||||
|
parentSection,
|
||||||
|
...this._getDeviceSections(remainingLinks, childSection),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base case: no more parent-child relationships to process
|
||||||
|
return [deviceNodes];
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
height: calc(
|
||||||
|
var(--row-size, 8) *
|
||||||
|
(var(--row-height, 50px) + var(--row-gap, 0px)) - var(--row-gap, 0px)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-water-sankey-card": HuiWaterSankeyCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,7 @@ const LAZY_LOAD_TYPES = {
|
|||||||
"energy-usage-graph": () =>
|
"energy-usage-graph": () =>
|
||||||
import("../cards/energy/hui-energy-usage-graph-card"),
|
import("../cards/energy/hui-energy-usage-graph-card"),
|
||||||
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
|
||||||
|
"water-sankey": () => import("../cards/water/hui-water-sankey-card"),
|
||||||
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
"entity-filter": () => import("../cards/hui-entity-filter-card"),
|
||||||
error: () => import("../cards/hui-error-card"),
|
error: () => import("../cards/hui-error-card"),
|
||||||
"home-summary": () => import("../cards/hui-home-summary-card"),
|
"home-summary": () => import("../cards/hui-home-summary-card"),
|
||||||
|
|||||||
@@ -3208,6 +3208,22 @@
|
|||||||
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
|
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
|
||||||
"no_upstream_devices": "No eligible upstream devices"
|
"no_upstream_devices": "No eligible upstream devices"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"device_consumption_water": {
|
||||||
|
"title": "Individual water devices",
|
||||||
|
"sub": "Tracking the water usage of individual devices allows Home Assistant to break down your water usage by device.",
|
||||||
|
"learn_more": "More information on how to get started.",
|
||||||
|
"devices": "Devices",
|
||||||
|
"add_device": "Add device",
|
||||||
|
"dialog": {
|
||||||
|
"header": "Add a water device",
|
||||||
|
"display_name": "Display name",
|
||||||
|
"device_consumption_water": "Device water consumption",
|
||||||
|
"selected_stat_intro": "Select the water sensor that measures the device's water usage in either of {unit}.",
|
||||||
|
"included_in_device": "Upstream device",
|
||||||
|
"included_in_device_helper": "If this device is already counted by another device (such as a water meter measured by the main water supply), selecting the upstream device prevents duplicate water tracking.",
|
||||||
|
"no_upstream_devices": "No eligible upstream devices"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"helpers": {
|
"helpers": {
|
||||||
@@ -9419,7 +9435,8 @@
|
|||||||
"energy_sources_table_title": "Sources",
|
"energy_sources_table_title": "Sources",
|
||||||
"energy_devices_graph_title": "Individual devices total usage",
|
"energy_devices_graph_title": "Individual devices total usage",
|
||||||
"energy_devices_detail_graph_title": "Individual devices detail usage",
|
"energy_devices_detail_graph_title": "Individual devices detail usage",
|
||||||
"energy_sankey_title": "Energy flow"
|
"energy_sankey_title": "Energy flow",
|
||||||
|
"water_sankey_title": "Water flow"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"history": {
|
"history": {
|
||||||
|
|||||||
Reference in New Issue
Block a user