mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +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,
|
isLastDayOfMonth,
|
||||||
} from "date-fns";
|
} from "date-fns";
|
||||||
import { Collection, getCollection } from "home-assistant-js-websocket";
|
import { Collection, getCollection } from "home-assistant-js-websocket";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import {
|
import {
|
||||||
calcDate,
|
calcDate,
|
||||||
calcDateProperty,
|
calcDateProperty,
|
||||||
@ -791,3 +792,147 @@ export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
|
|||||||
|
|
||||||
export const energyStatisticHelpUrl =
|
export const energyStatisticHelpUrl =
|
||||||
"/docs/energy/faq/#troubleshooting-missing-entities";
|
"/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 { classMap } from "lit/directives/class-map";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||||
|
import { getEnergyColor } from "./common/color";
|
||||||
import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base";
|
import { ChartDatasetExtra } from "../../../../components/chart/ha-chart-base";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
import {
|
import {
|
||||||
DeviceConsumptionEnergyPreference,
|
DeviceConsumptionEnergyPreference,
|
||||||
EnergyData,
|
EnergyData,
|
||||||
getEnergyDataCollection,
|
getEnergyDataCollection,
|
||||||
|
getSummedData,
|
||||||
|
computeConsumptionData,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
import {
|
import {
|
||||||
calculateStatisticSumGrowth,
|
calculateStatisticSumGrowth,
|
||||||
@ -75,6 +78,8 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
})
|
})
|
||||||
private _hiddenStats: string[] = [];
|
private _hiddenStats: string[] = [];
|
||||||
|
|
||||||
|
private _untrackedIndex?: number;
|
||||||
|
|
||||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
@ -152,17 +157,22 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _datasetHidden(ev) {
|
private _datasetHidden(ev) {
|
||||||
this._hiddenStats = [
|
const hiddenEntity =
|
||||||
...this._hiddenStats,
|
ev.detail.index === this._untrackedIndex
|
||||||
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption,
|
? "untracked"
|
||||||
];
|
: this._data!.prefs.device_consumption[ev.detail.index]
|
||||||
|
.stat_consumption;
|
||||||
|
this._hiddenStats = [...this._hiddenStats, hiddenEntity];
|
||||||
}
|
}
|
||||||
|
|
||||||
private _datasetUnhidden(ev) {
|
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(
|
this._hiddenStats = this._hiddenStats.filter(
|
||||||
(stat) =>
|
(stat) => stat !== hiddenEntity
|
||||||
stat !==
|
|
||||||
this._data!.prefs.device_consumption[ev.detail.index].stat_consumption
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -257,6 +267,33 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
|
|
||||||
datasetExtras.push(...processedDataExtras);
|
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) {
|
if (compareData) {
|
||||||
// Add empty dataset to align the bars
|
// Add empty dataset to align the bars
|
||||||
datasets.push({
|
datasets.push({
|
||||||
@ -289,6 +326,20 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
|
|
||||||
datasets.push(...processedCompareData);
|
datasets.push(...processedCompareData);
|
||||||
datasetExtras.push(...processedCompareDataExtras);
|
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;
|
this._start = energyData.start;
|
||||||
@ -303,6 +354,57 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
this._chartDatasetExtra = datasetExtras;
|
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(
|
private _processDataSet(
|
||||||
computedStyle: CSSStyleDeclaration,
|
computedStyle: CSSStyleDeclaration,
|
||||||
statistics: Statistics,
|
statistics: Statistics,
|
||||||
|
@ -7155,7 +7155,8 @@
|
|||||||
"charts": {
|
"charts": {
|
||||||
"stat_house_energy_meter": "Total energy consumption",
|
"stat_house_energy_meter": "Total energy consumption",
|
||||||
"solar": "Solar",
|
"solar": "Solar",
|
||||||
"by_device": "Consumption by device"
|
"by_device": "Consumption by device",
|
||||||
|
"untracked_consumption": "Untracked consumption"
|
||||||
},
|
},
|
||||||
"cards": {
|
"cards": {
|
||||||
"energy_usage_graph_title": "Energy usage",
|
"energy_usage_graph_title": "Energy usage",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user