Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for hierarchy of individual energy devices #23185

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/chart/ha-chart-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface Tooltip
export interface ChartDatasetExtra {
show_legend?: boolean;
legend_label?: string;
id?: string;
}

@customElement("ha-chart-base")
Expand Down
1 change: 1 addition & 0 deletions src/data/energy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export interface DeviceConsumptionEnergyPreference {
// This is an ever increasing value
stat_consumption: string;
name?: string;
parent_stat?: string;
}

export interface FlowFromGridSourceEnergyPreference {
Expand Down
13 changes: 12 additions & 1 deletion src/panels/config/energy/components/ha-energy-device-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export class EnergyDeviceSettings extends LitElement {
const origDevice: DeviceConsumptionEnergyPreference =
ev.currentTarget.closest(".row").device;
showEnergySettingsDeviceDialog(this, {
statsMetadata: this.statsMetadata,
device: { ...origDevice },
device_consumptions: this.preferences
.device_consumption as DeviceConsumptionEnergyPreference[],
Expand All @@ -168,6 +169,7 @@ export class EnergyDeviceSettings extends LitElement {

private _addDevice() {
showEnergySettingsDeviceDialog(this, {
statsMetadata: this.statsMetadata,
device_consumptions: this.preferences
.device_consumption as DeviceConsumptionEnergyPreference[],
saveCallback: async (device) => {
Expand All @@ -193,12 +195,21 @@ export class EnergyDeviceSettings extends LitElement {
}

try {
await this._savePreferences({
const newPrefs = {
...this.preferences,
device_consumption: this.preferences.device_consumption.filter(
(device) => device !== deviceToDelete
),
};
newPrefs.device_consumption.forEach((d, idx) => {
if (d.parent_stat === deviceToDelete.stat_consumption) {
newPrefs.device_consumption[idx] = {
...newPrefs.device_consumption[idx],
};
delete newPrefs.device_consumption[idx].parent_stat;
}
});
await this._savePreferences(newPrefs);
} catch (err: any) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ 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-formfield";
import "../../../../components/ha-radio";
import "../../../../components/ha-select";
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";
Expand All @@ -36,19 +39,47 @@ export class DialogEnergyDeviceSettings

private _excludeList?: string[];

private _possibleParents: DeviceConsumptionEnergyPreference[] = [];

public async showDialog(
params: EnergySettingsDeviceDialogParams
): Promise<void> {
this._params = params;
this._device = this._params.device;
if (this._device) {
this._computePossibleParents();
}
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._device = this._params.device;
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.parent_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 &&
!children.includes(d.stat_consumption)
);
}

public closeDialog() {
this._params = undefined;
this._device = undefined;
Expand Down Expand Up @@ -105,10 +136,46 @@ export class DialogEnergyDeviceSettings
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.dialog.parent_device"
)}
.value=${this._device?.parent_stat || ""}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.parent_device_helper"
)}
.disabled=${!this._device}
@selected=${this._parentSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
clearable
>
${this._possibleParents.map(
(stat) => html`
<mwc-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</mwc-list-item
>
`
)}
</ha-select>

<mwc-button @click=${this.closeDialog} slot="secondaryAction">
${this.hass.localize("ui.common.cancel")}
</mwc-button>
Expand All @@ -129,6 +196,7 @@ export class DialogEnergyDeviceSettings
return;
}
this._device = { stat_consumption: ev.detail.value };
this._computePossibleParents();
}

private _nameChanged(ev) {
Expand All @@ -142,6 +210,17 @@ export class DialogEnergyDeviceSettings
this._device = newDevice;
}

private _parentSelected(ev) {
const newDevice = {
...this._device!,
parent_stat: ev.target!.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.parent_stat) {
delete newDevice.parent_stat;
}
this._device = newDevice;
}

private async _save() {
try {
await this._params!.saveCallback(this._device!);
Expand All @@ -158,6 +237,10 @@ export class DialogEnergyDeviceSettings
ha-statistic-picker {
width: 100%;
}
ha-select {
margin-top: 16px;
width: 100%;
}
ha-textfield {
margin-top: 16px;
width: 100%;
Expand Down
1 change: 1 addition & 0 deletions src/panels/config/energy/dialogs/show-dialogs-energy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface EnergySettingsWaterDialogParams {
export interface EnergySettingsDeviceDialogParams {
device?: DeviceConsumptionEnergyPreference;
device_consumptions: DeviceConsumptionEnergyPreference[];
statsMetadata?: Record<string, StatisticsMetaData>;
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,16 @@ export class HuiEnergyDevicesDetailGraphCard

const computedStyle = getComputedStyle(this);

const devices = energyData.prefs.device_consumption;

const childMap: Record<string, string[]> = {};
devices.forEach((d) => {
if (d.parent_stat) {
childMap[d.parent_stat] = childMap[d.parent_stat] || [];
childMap[d.parent_stat].push(d.stat_consumption);
}
});

const growthValues = {};
energyData.prefs.device_consumption.forEach((device) => {
const value =
Expand All @@ -247,6 +257,22 @@ export class HuiEnergyDevicesDetailGraphCard
);
sorted_devices.sort((a, b) => growthValues[b] - growthValues[a]);

const ordered_devices: string[] = [];

// Recursively build an ordered list of devices, where each device has all its children immediately following it.
function orderDevices(parent?: string) {
sorted_devices.forEach((device) => {
const parent_stat = energyData.prefs.device_consumption.find(
(prf) => prf.stat_consumption === device
)?.parent_stat;
if ((!parent && !parent_stat) || parent === parent_stat) {
ordered_devices.push(device);
orderDevices(device);
}
});
}
orderDevices();

const datasets: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
const datasetExtras: ChartDatasetExtra[] = [];

Expand All @@ -256,7 +282,8 @@ export class HuiEnergyDevicesDetailGraphCard
data,
energyData.statsMetadata,
energyData.prefs.device_consumption,
sorted_devices
ordered_devices,
childMap
);

datasets.push(...processedData);
Expand All @@ -283,7 +310,9 @@ export class HuiEnergyDevicesDetailGraphCard
this._processUntracked(
computedStyle,
processedData,
processedDataExtras,
consumptionData,
energyData.prefs.device_consumption,
false
);
datasets.push(untrackedData);
Expand Down Expand Up @@ -316,7 +345,8 @@ export class HuiEnergyDevicesDetailGraphCard
compareData,
energyData.statsMetadata,
energyData.prefs.device_consumption,
sorted_devices,
ordered_devices,
childMap,
true
);

Expand All @@ -330,7 +360,9 @@ export class HuiEnergyDevicesDetailGraphCard
} = this._processUntracked(
computedStyle,
processedCompareData,
processedCompareDataExtras,
consumptionCompareData,
energyData.prefs.device_consumption,
true
);
datasets.push(untrackedCompareData);
Expand All @@ -353,16 +385,27 @@ export class HuiEnergyDevicesDetailGraphCard
private _processUntracked(
computedStyle: CSSStyleDeclaration,
processedData,
processedDataExtras,
consumptionData,
deviceConsumptionPrefs,
compare: boolean
): { dataset; datasetExtra } {
const totalDeviceConsumption: { [start: number]: number } = {};

processedData.forEach((device) => {
device.data.forEach((datapoint) => {
totalDeviceConsumption[datapoint.x] =
(totalDeviceConsumption[datapoint.x] || 0) + datapoint.y;
});
processedData.forEach((device, idx) => {
const stat = deviceConsumptionPrefs.find(
(pref) => pref.stat_consumption === processedDataExtras[idx].id
);

// If a child is hidden, don't count it in the total, because the parent device will grow to encompass that consumption.
const hiddenChild =
stat.parent_stat && this._hiddenStats.includes(stat.stat_consumption);
if (!hiddenChild) {
device.data.forEach((datapoint) => {
totalDeviceConsumption[datapoint.x] =
(totalDeviceConsumption[datapoint.x] || 0) + datapoint.y;
});
}
});

const untrackedConsumption: { x: number; y: number }[] = [];
Expand Down Expand Up @@ -409,6 +452,7 @@ export class HuiEnergyDevicesDetailGraphCard
statisticsMetaData: Record<string, StatisticsMetaData>,
devices: DeviceConsumptionEnergyPreference[],
sorted_devices: string[],
childMap: Record<string, string[]>,
compare = false
) {
const data: ChartDataset<"bar", ScatterDataPoint[]>[] = [];
Expand All @@ -421,7 +465,7 @@ export class HuiEnergyDevicesDetailGraphCard

const consumptionData: ScatterDataPoint[] = [];

// Process gas consumption data.
// Process device consumption data.
if (source.stat_consumption in statistics) {
const stats = statistics[source.stat_consumption];
let end;
Expand All @@ -434,9 +478,26 @@ export class HuiEnergyDevicesDetailGraphCard
continue;
}
const date = new Date(point.start);

let sumChildren = 0;
const sumVisibleChildren = (parent) => {
const children = childMap[parent] || [];
children.forEach((c) => {
if (this._hiddenStats.includes(c)) {
sumVisibleChildren(c);
} else {
const cStats = statistics[c];
sumChildren +=
cStats?.find((cStat) => cStat.start === point.start)
?.change || 0;
}
});
};
sumVisibleChildren(source.stat_consumption);

consumptionData.push({
x: date.getTime(),
y: point.change,
y: point.change - sumChildren,
});
prevStart = point.start;
end = point.end;
Expand Down Expand Up @@ -472,7 +533,10 @@ export class HuiEnergyDevicesDetailGraphCard
pointStyle: compare ? false : "circle",
xAxisID: compare ? "xAxisCompare" : undefined,
});
dataExtras.push({ show_legend: !compare && !itemExceedsMax });
dataExtras.push({
show_legend: !compare && !itemExceedsMax,
id: source.stat_consumption,
});
});
return { data, dataExtras };
}
Expand Down
4 changes: 3 additions & 1 deletion src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2802,7 +2802,9 @@
"header": "Add a device",
"display_name": "Display name",
"device_consumption_energy": "Device energy consumption",
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}."
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.",
"parent_device": "Parent device",
"parent_device_helper": "If consumption for this device is also included in another device (for example, if this is a smart switch which is also counted by a smart circuit breaker), select that upstream device here. This will prevent double counting of the consumption"
}
}
},
Expand Down
Loading