mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
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:
parent
2f68ee0efc
commit
93ee5de1b4
@ -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;
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user