Plot 'untracked consumption' on devices detail energy graph (#21632)

* Plot 'untracked consumption' on devices detail energy graph

* skip when there are no energy sources

* rename variable
This commit is contained in:
karwosts 2024-08-28 05:24:19 -07:00 committed by GitHub
parent 2f68ee0efc
commit 93ee5de1b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 256 additions and 8 deletions

View File

@ -11,6 +11,7 @@ import {
isLastDayOfMonth,
} from "date-fns";
import { Collection, getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import {
calcDate,
calcDateProperty,
@ -791,3 +792,147 @@ export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
export const energyStatisticHelpUrl =
"/docs/energy/faq/#troubleshooting-missing-entities";
interface EnergySumData {
to_grid?: { [start: number]: number };
from_grid?: { [start: number]: number };
to_battery?: { [start: number]: number };
from_battery?: { [start: number]: number };
solar?: { [start: number]: number };
}
interface EnergyConsumptionData {
total: { [start: number]: number };
}
export const getSummedData = memoizeOne(
(
data: EnergyData
): { summedData: EnergySumData; compareSummedData?: EnergySumData } => {
const summedData = getSummedDataPartial(data);
const compareSummedData = data.statsCompare
? getSummedDataPartial(data, true)
: undefined;
return { summedData, compareSummedData };
}
);
const getSummedDataPartial = (
data: EnergyData,
compare?: boolean
): EnergySumData => {
const statIds: {
to_grid?: string[];
from_grid?: string[];
solar?: string[];
to_battery?: string[];
from_battery?: string[];
} = {};
for (const source of data.prefs.energy_sources) {
if (source.type === "solar") {
if (statIds.solar) {
statIds.solar.push(source.stat_energy_from);
} else {
statIds.solar = [source.stat_energy_from];
}
continue;
}
if (source.type === "battery") {
if (statIds.to_battery) {
statIds.to_battery.push(source.stat_energy_to);
statIds.from_battery!.push(source.stat_energy_from);
} else {
statIds.to_battery = [source.stat_energy_to];
statIds.from_battery = [source.stat_energy_from];
}
continue;
}
if (source.type !== "grid") {
continue;
}
// grid source
for (const flowFrom of source.flow_from) {
if (statIds.from_grid) {
statIds.from_grid.push(flowFrom.stat_energy_from);
} else {
statIds.from_grid = [flowFrom.stat_energy_from];
}
}
for (const flowTo of source.flow_to) {
if (statIds.to_grid) {
statIds.to_grid.push(flowTo.stat_energy_to);
} else {
statIds.to_grid = [flowTo.stat_energy_to];
}
}
}
const summedData: EnergySumData = {};
Object.entries(statIds).forEach(([key, subStatIds]) => {
const totalStats: { [start: number]: number } = {};
const sets: { [statId: string]: { [start: number]: number } } = {};
subStatIds!.forEach((id) => {
const stats = compare ? data.statsCompare[id] : data.stats[id];
if (!stats) {
return;
}
const set = {};
stats.forEach((stat) => {
if (stat.change === null || stat.change === undefined) {
return;
}
const val = stat.change;
// Get total of solar and to grid to calculate the solar energy used
totalStats[stat.start] =
stat.start in totalStats ? totalStats[stat.start] + val : val;
});
sets[id] = set;
});
summedData[key] = totalStats;
});
return summedData;
};
export const computeConsumptionData = memoizeOne(
(
data: EnergySumData,
compareData?: EnergySumData
): {
consumption: EnergyConsumptionData;
compareConsumption?: EnergyConsumptionData;
} => {
const consumption = computeConsumptionDataPartial(data);
const compareConsumption = compareData
? computeConsumptionDataPartial(compareData)
: undefined;
return { consumption, compareConsumption };
}
);
const computeConsumptionDataPartial = (
data: EnergySumData
): EnergyConsumptionData => {
const outData: EnergyConsumptionData = { total: {} };
Object.keys(data).forEach((type) => {
Object.keys(data[type]).forEach((start) => {
if (outData.total[start] === undefined) {
const consumption =
(data.from_grid?.[start] || 0) +
(data.solar?.[start] || 0) +
(data.from_battery?.[start] || 0) -
(data.to_grid?.[start] || 0) -
(data.to_battery?.[start] || 0);
outData.total[start] = consumption;
}
});
});
return outData;
};

View File

@ -18,12 +18,15 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { getEnergyColor } from "./common/color";
import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
DeviceConsumptionEnergyPreference,
EnergyData,
getEnergyDataCollection,
getSummedData,
computeConsumptionData,
} from "../../../../data/energy";
import {
calculateStatisticSumGrowth,
@ -75,6 +78,8 @@ export class HuiEnergyDevicesDetailGraphCard
})
private _hiddenStats: string[] = [];
private _untrackedIndex?: number;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
@ -152,17 +157,22 @@ export class HuiEnergyDevicesDetailGraphCard
}
private _datasetHidden(ev) {
this._hiddenStats = [
...this._hiddenStats,
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption,
];
const hiddenEntity =
ev.detail.index === this._untrackedIndex
? "untracked"
: this._data!.prefs.device_consumption[ev.detail.index]
.stat_consumption;
this._hiddenStats = [...this._hiddenStats, hiddenEntity];
}
private _datasetUnhidden(ev) {
const hiddenEntity =
ev.detail.index === this._untrackedIndex
? "untracked"
: this._data!.prefs.device_consumption[ev.detail.index]
.stat_consumption;
this._hiddenStats = this._hiddenStats.filter(
(stat) =>
stat !==
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption
(stat) => stat !== hiddenEntity
);
}
@ -257,6 +267,33 @@ export class HuiEnergyDevicesDetailGraphCard
datasetExtras.push(...processedDataExtras);
const { summedData, compareSummedData } = getSummedData(energyData);
const showUntracked =
"from_grid" in summedData ||
"solar" in summedData ||
"from_battery" in summedData;
const {
consumption: consumptionData,
compareConsumption: consumptionCompareData,
} = showUntracked
? computeConsumptionData(summedData, compareSummedData)
: { consumption: undefined, compareConsumption: undefined };
if (showUntracked) {
this._untrackedIndex = datasets.length;
const { dataset: untrackedData, datasetExtra: untrackedDataExtra } =
this._processUntracked(
computedStyle,
processedData,
consumptionData,
false
);
datasets.push(untrackedData);
datasetExtras.push(untrackedDataExtra);
}
if (compareData) {
// Add empty dataset to align the bars
datasets.push({
@ -289,6 +326,20 @@ export class HuiEnergyDevicesDetailGraphCard
datasets.push(...processedCompareData);
datasetExtras.push(...processedCompareDataExtras);
if (showUntracked) {
const {
dataset: untrackedCompareData,
datasetExtra: untrackedCompareDataExtra,
} = this._processUntracked(
computedStyle,
processedCompareData,
consumptionCompareData,
true
);
datasets.push(untrackedCompareData);
datasetExtras.push(untrackedCompareDataExtra);
}
}
this._start = energyData.start;
@ -303,6 +354,57 @@ export class HuiEnergyDevicesDetailGraphCard
this._chartDatasetExtra = datasetExtras;
}
private _processUntracked(
computedStyle: CSSStyleDeclaration,
processedData,
consumptionData,
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;
});
});
const untrackedConsumption: { x: number; y: number }[] = [];
Object.keys(consumptionData.total).forEach((time) => {
untrackedConsumption.push({
x: Number(time),
y: consumptionData.total[time] - (totalDeviceConsumption[time] || 0),
});
});
const dataset = {
label: this.hass.localize("ui.panel.energy.charts.untracked_consumption"),
hidden: this._hiddenStats.includes("untracked"),
borderColor: getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
false,
compare,
"--state-unavailable-color"
),
backgroundColor: getEnergyColor(
computedStyle,
this.hass.themes.darkMode,
true,
compare,
"--state-unavailable-color"
),
data: untrackedConsumption,
order: 1 + this._untrackedIndex!,
stack: "devices",
pointStyle: compare ? false : "circle",
xAxisID: compare ? "xAxisCompare" : undefined,
};
const datasetExtra = {
show_legend: !compare,
};
return { dataset, datasetExtra };
}
private _processDataSet(
computedStyle: CSSStyleDeclaration,
statistics: Statistics,

View File

@ -7155,7 +7155,8 @@
"charts": {
"stat_house_energy_meter": "Total energy consumption",
"solar": "Solar",
"by_device": "Consumption by device"
"by_device": "Consumption by device",
"untracked_consumption": "Untracked consumption"
},
"cards": {
"energy_usage_graph_title": "Energy usage",