mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Centralize energy usage calculations (#25197)
* Centralize energy usage calculations * addl tests * test organization * Update src/data/energy.ts Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com> * Centralize more equations --------- Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
This commit is contained in:
parent
bc582db7fc
commit
92353ebed5
@ -795,10 +795,28 @@ export interface EnergySumData {
|
||||
from_battery?: number;
|
||||
solar?: number;
|
||||
};
|
||||
timestamps: number[];
|
||||
}
|
||||
|
||||
interface EnergyConsumptionData {
|
||||
total: Record<number, number>;
|
||||
export interface EnergyConsumptionData {
|
||||
used_total: Record<number, number>;
|
||||
grid_to_battery: Record<number, number>;
|
||||
battery_to_grid: Record<number, number>;
|
||||
solar_to_battery: Record<number, number>;
|
||||
solar_to_grid: Record<number, number>;
|
||||
used_solar: Record<number, number>;
|
||||
used_grid: Record<number, number>;
|
||||
used_battery: Record<number, number>;
|
||||
total: {
|
||||
used_total: number;
|
||||
grid_to_battery: number;
|
||||
battery_to_grid: number;
|
||||
solar_to_battery: number;
|
||||
solar_to_grid: number;
|
||||
used_solar: number;
|
||||
used_grid: number;
|
||||
used_battery: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const getSummedData = memoizeOne(
|
||||
@ -867,7 +885,8 @@ const getSummedDataPartial = (
|
||||
}
|
||||
}
|
||||
|
||||
const summedData: EnergySumData = { total: {} };
|
||||
const summedData: EnergySumData = { total: {}, timestamps: [] };
|
||||
const timestamps = new Set<number>();
|
||||
Object.entries(statIds).forEach(([key, subStatIds]) => {
|
||||
const totalStats: Record<number, number> = {};
|
||||
const sets: Record<string, Record<number, number>> = {};
|
||||
@ -886,6 +905,7 @@ const getSummedDataPartial = (
|
||||
sum += val;
|
||||
totalStats[stat.start] =
|
||||
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
||||
timestamps.add(stat.start);
|
||||
});
|
||||
sets[id] = set;
|
||||
});
|
||||
@ -893,6 +913,8 @@ const getSummedDataPartial = (
|
||||
summedData.total[key] = sum;
|
||||
});
|
||||
|
||||
summedData.timestamps = Array.from(timestamps).sort();
|
||||
|
||||
return summedData;
|
||||
};
|
||||
|
||||
@ -915,25 +937,142 @@ export const computeConsumptionData = memoizeOne(
|
||||
const computeConsumptionDataPartial = (
|
||||
data: EnergySumData
|
||||
): EnergyConsumptionData => {
|
||||
const outData: EnergyConsumptionData = { total: {} };
|
||||
const outData: EnergyConsumptionData = {
|
||||
used_total: {},
|
||||
grid_to_battery: {},
|
||||
battery_to_grid: {},
|
||||
solar_to_battery: {},
|
||||
solar_to_grid: {},
|
||||
used_solar: {},
|
||||
used_grid: {},
|
||||
used_battery: {},
|
||||
total: {
|
||||
used_total: 0,
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: 0,
|
||||
used_solar: 0,
|
||||
used_grid: 0,
|
||||
used_battery: 0,
|
||||
},
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
data.timestamps.forEach((t) => {
|
||||
const used_total =
|
||||
(data.from_grid?.[t] || 0) +
|
||||
(data.solar?.[t] || 0) +
|
||||
(data.from_battery?.[t] || 0) -
|
||||
(data.to_grid?.[t] || 0) -
|
||||
(data.to_battery?.[t] || 0);
|
||||
|
||||
outData.used_total[t] = used_total;
|
||||
outData.total.used_total += used_total;
|
||||
const {
|
||||
grid_to_battery,
|
||||
battery_to_grid,
|
||||
used_solar,
|
||||
used_grid,
|
||||
used_battery,
|
||||
solar_to_battery,
|
||||
solar_to_grid,
|
||||
} = computeConsumptionSingle({
|
||||
from_grid: data.from_grid && (data.from_grid[t] ?? 0),
|
||||
to_grid: data.to_grid && (data.to_grid[t] ?? 0),
|
||||
solar: data.solar && (data.solar[t] ?? 0),
|
||||
to_battery: data.to_battery && (data.to_battery[t] ?? 0),
|
||||
from_battery: data.from_battery && (data.from_battery[t] ?? 0),
|
||||
});
|
||||
|
||||
outData.grid_to_battery[t] = grid_to_battery;
|
||||
outData.total.grid_to_battery += grid_to_battery;
|
||||
outData.battery_to_grid![t] = battery_to_grid;
|
||||
outData.total.battery_to_grid += battery_to_grid;
|
||||
outData.used_battery![t] = used_battery;
|
||||
outData.total.used_battery += used_battery;
|
||||
outData.used_grid![t] = used_grid;
|
||||
outData.total.used_grid += used_grid;
|
||||
outData.used_solar![t] = used_solar;
|
||||
outData.total.used_solar += used_solar;
|
||||
outData.solar_to_battery[t] = solar_to_battery;
|
||||
outData.total.solar_to_battery += solar_to_battery;
|
||||
outData.solar_to_grid[t] = solar_to_grid;
|
||||
outData.total.solar_to_grid += solar_to_grid;
|
||||
});
|
||||
|
||||
return outData;
|
||||
};
|
||||
|
||||
export const computeConsumptionSingle = (data: {
|
||||
from_grid: number | undefined;
|
||||
to_grid: number | undefined;
|
||||
solar: number | undefined;
|
||||
to_battery: number | undefined;
|
||||
from_battery: number | undefined;
|
||||
}): {
|
||||
grid_to_battery: number;
|
||||
battery_to_grid: number;
|
||||
solar_to_battery: number;
|
||||
solar_to_grid: number;
|
||||
used_solar: number;
|
||||
used_grid: number;
|
||||
used_battery: number;
|
||||
} => {
|
||||
const to_grid = data.to_grid;
|
||||
const to_battery = data.to_battery;
|
||||
const solar = data.solar;
|
||||
const from_grid = data.from_grid;
|
||||
const from_battery = data.from_battery;
|
||||
|
||||
let used_solar = 0;
|
||||
let grid_to_battery = 0;
|
||||
let battery_to_grid = 0;
|
||||
let solar_to_battery = 0;
|
||||
let solar_to_grid = 0;
|
||||
let used_battery = 0;
|
||||
let used_grid = 0;
|
||||
if ((to_grid != null || to_battery != null) && solar != null) {
|
||||
used_solar = (solar || 0) - (to_grid || 0) - (to_battery || 0);
|
||||
if (used_solar < 0) {
|
||||
if (to_battery != null) {
|
||||
grid_to_battery = used_solar * -1;
|
||||
if (grid_to_battery > (from_grid || 0)) {
|
||||
battery_to_grid = grid_to_battery - (from_grid || 0);
|
||||
grid_to_battery = from_grid || 0;
|
||||
}
|
||||
}
|
||||
used_solar = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (from_battery != null) {
|
||||
used_battery = (from_battery || 0) - battery_to_grid;
|
||||
}
|
||||
|
||||
if (from_grid != null) {
|
||||
used_grid = from_grid - grid_to_battery;
|
||||
}
|
||||
|
||||
if (solar != null) {
|
||||
if (to_battery != null) {
|
||||
solar_to_battery = Math.max(0, (to_battery || 0) - grid_to_battery);
|
||||
}
|
||||
if (to_grid != null) {
|
||||
solar_to_grid = Math.max(0, (to_grid || 0) - battery_to_grid);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
used_solar,
|
||||
used_grid,
|
||||
used_battery,
|
||||
grid_to_battery,
|
||||
battery_to_grid,
|
||||
solar_to_battery,
|
||||
solar_to_grid,
|
||||
};
|
||||
};
|
||||
|
||||
export const formatConsumptionShort = (
|
||||
hass: HomeAssistant,
|
||||
consumption: number | null,
|
||||
|
@ -347,12 +347,13 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
);
|
||||
|
||||
const untrackedConsumption: BarSeriesOption["data"] = [];
|
||||
Object.keys(consumptionData.total)
|
||||
Object.keys(consumptionData.used_total)
|
||||
.sort((a, b) => Number(a) - Number(b))
|
||||
.forEach((time) => {
|
||||
const ts = Number(time);
|
||||
const value =
|
||||
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
||||
consumptionData.used_total[time] -
|
||||
(totalDeviceConsumption[time] || 0);
|
||||
const dataPoint: number[] = [ts, value];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
|
@ -27,6 +27,7 @@ import {
|
||||
getEnergyWaterUnit,
|
||||
formatConsumptionShort,
|
||||
getSummedData,
|
||||
computeConsumptionData,
|
||||
} from "../../../../data/energy";
|
||||
import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
@ -111,6 +112,10 @@ class HuiEnergyDistrubutionCard
|
||||
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
||||
|
||||
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
||||
const { consumption, compareConsumption: __ } = computeConsumptionData(
|
||||
summedData,
|
||||
undefined
|
||||
);
|
||||
|
||||
const totalFromGrid = summedData.total.from_grid ?? 0;
|
||||
|
||||
@ -154,55 +159,32 @@ class HuiEnergyDistrubutionCard
|
||||
|
||||
let solarConsumption: number | null = null;
|
||||
if (hasSolarProduction) {
|
||||
solarConsumption =
|
||||
(totalSolarProduction || 0) -
|
||||
(returnedToGrid || 0) -
|
||||
(totalBatteryIn || 0);
|
||||
solarConsumption = consumption.total.used_solar;
|
||||
}
|
||||
|
||||
let batteryFromGrid: null | number = null;
|
||||
let batteryToGrid: null | number = null;
|
||||
if (solarConsumption !== null && solarConsumption < 0) {
|
||||
// What we returned to the grid and what went in to the battery is more than produced,
|
||||
// so we have used grid energy to fill the battery
|
||||
// or returned battery energy to the grid
|
||||
if (hasBattery) {
|
||||
batteryFromGrid = solarConsumption * -1;
|
||||
if (batteryFromGrid > totalFromGrid) {
|
||||
batteryToGrid = batteryFromGrid - totalFromGrid;
|
||||
batteryFromGrid = totalFromGrid;
|
||||
}
|
||||
}
|
||||
solarConsumption = 0;
|
||||
if (hasBattery) {
|
||||
batteryToGrid = consumption.total.battery_to_grid;
|
||||
batteryFromGrid = consumption.total.grid_to_battery;
|
||||
}
|
||||
|
||||
let solarToBattery: null | number = null;
|
||||
let solarToGrid: null | number = null;
|
||||
if (hasSolarProduction) {
|
||||
solarToGrid = consumption.total.solar_to_grid;
|
||||
}
|
||||
if (hasSolarProduction && hasBattery) {
|
||||
if (!batteryToGrid) {
|
||||
batteryToGrid = Math.max(
|
||||
0,
|
||||
(returnedToGrid || 0) -
|
||||
(totalSolarProduction || 0) -
|
||||
(totalBatteryIn || 0) -
|
||||
(batteryFromGrid || 0)
|
||||
);
|
||||
}
|
||||
solarToBattery = totalBatteryIn! - (batteryFromGrid || 0);
|
||||
} else if (!hasSolarProduction && hasBattery) {
|
||||
batteryToGrid = returnedToGrid;
|
||||
solarToBattery = consumption.total.solar_to_battery;
|
||||
}
|
||||
|
||||
let batteryConsumption: number | null = null;
|
||||
if (hasBattery) {
|
||||
batteryConsumption = (totalBatteryOut || 0) - (batteryToGrid || 0);
|
||||
batteryConsumption = Math.max(consumption.total.used_battery, 0);
|
||||
}
|
||||
|
||||
const gridConsumption = Math.max(0, totalFromGrid - (batteryFromGrid || 0));
|
||||
const gridConsumption = Math.max(consumption.total.used_grid, 0);
|
||||
|
||||
const totalHomeConsumption = Math.max(
|
||||
0,
|
||||
gridConsumption + (solarConsumption || 0) + (batteryConsumption || 0)
|
||||
);
|
||||
const totalHomeConsumption = Math.max(0, consumption.total.used_total);
|
||||
|
||||
let homeSolarCircumference: number | undefined;
|
||||
if (hasSolarProduction) {
|
||||
@ -262,7 +244,7 @@ class HuiEnergyDistrubutionCard
|
||||
const totalLines =
|
||||
gridConsumption +
|
||||
(solarConsumption || 0) +
|
||||
(returnedToGrid ? returnedToGrid - (batteryToGrid || 0) : 0) +
|
||||
(solarToGrid || 0) +
|
||||
(solarToBattery || 0) +
|
||||
(batteryConsumption || 0) +
|
||||
(batteryFromGrid || 0) +
|
||||
@ -662,18 +644,14 @@ class HuiEnergyDistrubutionCard
|
||||
d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100"
|
||||
vector-effect="non-scaling-stroke"
|
||||
></path>
|
||||
${returnedToGrid && hasSolarProduction && this._animate
|
||||
${solarToGrid && this._animate
|
||||
? svg`<circle
|
||||
r="1"
|
||||
class="return"
|
||||
vector-effect="non-scaling-stroke"
|
||||
>
|
||||
<animateMotion
|
||||
dur="${
|
||||
6 -
|
||||
((returnedToGrid - (batteryToGrid || 0)) / totalLines) *
|
||||
6
|
||||
}s"
|
||||
dur="${6 - (solarToGrid / totalLines) * 6}s"
|
||||
repeatCount="indefinite"
|
||||
calcMode="linear"
|
||||
>
|
||||
|
@ -10,6 +10,7 @@ import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
} from "../../../../data/energy";
|
||||
@ -76,70 +77,14 @@ class HuiEnergySelfSufficiencyGaugeCard
|
||||
|
||||
// The strategy only includes this card if we have a grid.
|
||||
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
||||
|
||||
const hasSolarProduction = summedData.solar !== undefined;
|
||||
const hasBattery =
|
||||
summedData.to_battery !== undefined ||
|
||||
summedData.from_battery !== undefined;
|
||||
const hasReturnToGrid = summedData.to_grid !== undefined;
|
||||
const { consumption, compareConsumption: __ } = computeConsumptionData(
|
||||
summedData,
|
||||
undefined
|
||||
);
|
||||
|
||||
const totalFromGrid = summedData.total.from_grid ?? 0;
|
||||
|
||||
let totalSolarProduction: number | null = null;
|
||||
|
||||
if (hasSolarProduction) {
|
||||
totalSolarProduction = summedData.total.solar ?? 0;
|
||||
}
|
||||
|
||||
let totalBatteryIn: number | null = null;
|
||||
let totalBatteryOut: number | null = null;
|
||||
|
||||
if (hasBattery) {
|
||||
totalBatteryIn = summedData.total.to_battery ?? 0;
|
||||
totalBatteryOut = summedData.total.from_battery ?? 0;
|
||||
}
|
||||
|
||||
let returnedToGrid: number | null = null;
|
||||
|
||||
if (hasReturnToGrid) {
|
||||
returnedToGrid = summedData.total.to_grid ?? 0;
|
||||
}
|
||||
|
||||
let solarConsumption: number | null = null;
|
||||
if (hasSolarProduction) {
|
||||
solarConsumption =
|
||||
(totalSolarProduction || 0) -
|
||||
(returnedToGrid || 0) -
|
||||
(totalBatteryIn || 0);
|
||||
}
|
||||
|
||||
let batteryFromGrid: null | number = null;
|
||||
let batteryToGrid: null | number = null;
|
||||
if (solarConsumption !== null && solarConsumption < 0) {
|
||||
// What we returned to the grid and what went in to the battery is more than produced,
|
||||
// so we have used grid energy to fill the battery
|
||||
// or returned battery energy to the grid
|
||||
if (hasBattery) {
|
||||
batteryFromGrid = solarConsumption * -1;
|
||||
if (batteryFromGrid > totalFromGrid) {
|
||||
batteryToGrid = batteryFromGrid - totalFromGrid;
|
||||
batteryFromGrid = totalFromGrid;
|
||||
}
|
||||
}
|
||||
solarConsumption = 0;
|
||||
}
|
||||
|
||||
let batteryConsumption: number | null = null;
|
||||
if (hasBattery) {
|
||||
batteryConsumption = (totalBatteryOut || 0) - (batteryToGrid || 0);
|
||||
}
|
||||
|
||||
const gridConsumption = Math.max(0, totalFromGrid - (batteryFromGrid || 0));
|
||||
|
||||
const totalHomeConsumption = Math.max(
|
||||
0,
|
||||
gridConsumption + (solarConsumption || 0) + (batteryConsumption || 0)
|
||||
);
|
||||
const totalHomeConsumption = Math.max(0, consumption.total.used_total);
|
||||
|
||||
let value: number | undefined;
|
||||
if (
|
||||
@ -147,7 +92,7 @@ class HuiEnergySelfSufficiencyGaugeCard
|
||||
totalHomeConsumption !== null &&
|
||||
totalHomeConsumption > 0
|
||||
) {
|
||||
value = (1 - totalFromGrid / totalHomeConsumption) * 100;
|
||||
value = (1 - Math.min(1, totalFromGrid / totalHomeConsumption)) * 100;
|
||||
}
|
||||
|
||||
return html`
|
||||
|
@ -14,8 +14,13 @@ import { getEnergyColor } from "./common/color";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import type { EnergyData, EnergySumData } from "../../../../data/energy";
|
||||
import type {
|
||||
EnergyData,
|
||||
EnergySumData,
|
||||
EnergyConsumptionData,
|
||||
} from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
} from "../../../../data/energy";
|
||||
@ -283,6 +288,10 @@ export class HuiEnergyUsageGraphCard
|
||||
this._compareEnd = energyData.endCompare;
|
||||
|
||||
const { summedData, compareSummedData } = getSummedData(energyData);
|
||||
const { consumption, compareConsumption } = computeConsumptionData(
|
||||
summedData,
|
||||
compareSummedData
|
||||
);
|
||||
|
||||
if (energyData.statsCompare) {
|
||||
datasets.push(
|
||||
@ -290,6 +299,7 @@ export class HuiEnergyUsageGraphCard
|
||||
energyData.statsCompare,
|
||||
energyData.statsMetadata,
|
||||
compareSummedData!,
|
||||
compareConsumption!,
|
||||
statIds,
|
||||
colorIndices,
|
||||
computedStyles,
|
||||
@ -315,6 +325,7 @@ export class HuiEnergyUsageGraphCard
|
||||
energyData.stats,
|
||||
energyData.statsMetadata,
|
||||
summedData,
|
||||
consumption,
|
||||
statIds,
|
||||
colorIndices,
|
||||
computedStyles,
|
||||
@ -333,6 +344,7 @@ export class HuiEnergyUsageGraphCard
|
||||
statistics: Statistics,
|
||||
statisticsMetaData: Record<string, StatisticsMetaData>,
|
||||
summedData: EnergySumData,
|
||||
consumptionData: EnergyConsumptionData,
|
||||
statIdsByCat: {
|
||||
to_grid?: string[] | undefined;
|
||||
from_grid?: string[] | undefined;
|
||||
@ -361,8 +373,7 @@ export class HuiEnergyUsageGraphCard
|
||||
} = {};
|
||||
|
||||
Object.entries(statIdsByCat).forEach(([key, statIds]) => {
|
||||
const add = !["solar", "from_battery"].includes(key);
|
||||
if (!add) {
|
||||
if (!["to_grid", "from_grid", "to_battery"].includes(key)) {
|
||||
return;
|
||||
}
|
||||
const sets: Record<string, Record<number, number>> = {};
|
||||
@ -387,47 +398,22 @@ export class HuiEnergyUsageGraphCard
|
||||
combinedData[key] = sets;
|
||||
});
|
||||
|
||||
const grid_to_battery = {};
|
||||
const battery_to_grid = {};
|
||||
if ((summedData.to_grid || summedData.to_battery) && summedData.solar) {
|
||||
const used_solar = {};
|
||||
for (const start of Object.keys(summedData.solar)) {
|
||||
used_solar[start] =
|
||||
(summedData.solar[start] || 0) -
|
||||
(summedData.to_grid?.[start] || 0) -
|
||||
(summedData.to_battery?.[start] || 0);
|
||||
if (used_solar[start] < 0) {
|
||||
if (summedData.to_battery) {
|
||||
grid_to_battery[start] = used_solar[start] * -1;
|
||||
if (grid_to_battery[start] > (summedData.from_grid?.[start] || 0)) {
|
||||
battery_to_grid[start] =
|
||||
grid_to_battery[start] - (summedData.from_grid?.[start] || 0);
|
||||
grid_to_battery[start] = summedData.from_grid?.[start];
|
||||
}
|
||||
}
|
||||
used_solar[start] = 0;
|
||||
}
|
||||
}
|
||||
combinedData.used_solar = { used_solar };
|
||||
}
|
||||
|
||||
if (summedData.from_battery) {
|
||||
if (summedData.to_grid) {
|
||||
const used_battery = {};
|
||||
for (const start of Object.keys(summedData.from_battery)) {
|
||||
used_battery[start] =
|
||||
(summedData.from_battery![start] || 0) -
|
||||
(battery_to_grid[start] || 0);
|
||||
}
|
||||
combinedData.used_battery = { used_battery };
|
||||
} else {
|
||||
combinedData.used_battery = { used_battery: summedData.from_battery };
|
||||
}
|
||||
}
|
||||
combinedData.used_solar = { used_solar: consumptionData.used_solar };
|
||||
combinedData.used_battery = {
|
||||
used_battery: consumptionData.used_battery,
|
||||
};
|
||||
|
||||
if (combinedData.from_grid && summedData.to_battery) {
|
||||
const used_grid = {};
|
||||
for (const start of Object.keys(grid_to_battery)) {
|
||||
// If we have to_battery and multiple grid sources in the same period, we
|
||||
// can't determine which source was used. So delete all the individual
|
||||
// sources and replace with a 'combined from grid' value.
|
||||
for (const [start, grid_to_battery] of Object.entries(
|
||||
consumptionData.grid_to_battery
|
||||
)) {
|
||||
if (!grid_to_battery) {
|
||||
continue;
|
||||
}
|
||||
let noOfSources = 0;
|
||||
let source: string;
|
||||
for (const [key, stats] of Object.entries(combinedData.from_grid)) {
|
||||
@ -440,30 +426,19 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
}
|
||||
if (noOfSources === 1) {
|
||||
combinedData.from_grid[source!][start] -= grid_to_battery[start] || 0;
|
||||
combinedData.from_grid[source!][start] =
|
||||
consumptionData.used_grid[start];
|
||||
} else {
|
||||
let total_from_grid = 0;
|
||||
Object.values(combinedData.from_grid).forEach((stats) => {
|
||||
total_from_grid += stats[start] || 0;
|
||||
delete stats[start];
|
||||
});
|
||||
used_grid[start] = total_from_grid - (grid_to_battery[start] || 0);
|
||||
used_grid[start] = consumptionData.used_grid[start];
|
||||
}
|
||||
}
|
||||
combinedData.used_grid = { used_grid };
|
||||
}
|
||||
|
||||
let allKeys: string[] = [];
|
||||
|
||||
Object.values(combinedData).forEach((sources) => {
|
||||
Object.values(sources).forEach((source) => {
|
||||
allKeys = allKeys.concat(Object.keys(source));
|
||||
});
|
||||
});
|
||||
|
||||
const uniqueKeys = Array.from(new Set(allKeys)).sort(
|
||||
(a, b) => Number(a) - Number(b)
|
||||
);
|
||||
const uniqueKeys = summedData.timestamps;
|
||||
|
||||
const compareTransform = getCompareTransform(
|
||||
this._start,
|
||||
@ -477,14 +452,14 @@ export class HuiEnergyUsageGraphCard
|
||||
for (const key of uniqueKeys) {
|
||||
const value = source[key] || 0;
|
||||
const dataPoint = [
|
||||
Number(key),
|
||||
new Date(key),
|
||||
value && ["to_grid", "to_battery"].includes(type)
|
||||
? -1 * value
|
||||
: value,
|
||||
];
|
||||
if (compare) {
|
||||
dataPoint[2] = dataPoint[0];
|
||||
dataPoint[0] = compareTransform(dataPoint[0]);
|
||||
dataPoint[0] = compareTransform(dataPoint[0] as Date);
|
||||
}
|
||||
points.push(dataPoint);
|
||||
}
|
||||
|
@ -8,7 +8,10 @@ import {
|
||||
DateFormat,
|
||||
TimeZone,
|
||||
} from "../../src/data/translation";
|
||||
import { formatConsumptionShort } from "../../src/data/energy";
|
||||
import {
|
||||
computeConsumptionSingle,
|
||||
formatConsumptionShort,
|
||||
} from "../../src/data/energy";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
describe("Energy Short Format Test", () => {
|
||||
@ -80,3 +83,292 @@ describe("Energy Short Format Test", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Energy Usage Calculation Tests", () => {
|
||||
it("Consuming Energy From the Grid", () => {
|
||||
[0, 5, 1000].forEach((x) => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: x,
|
||||
to_grid: undefined,
|
||||
solar: undefined,
|
||||
to_battery: undefined,
|
||||
from_battery: undefined,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 0,
|
||||
used_grid: x,
|
||||
used_battery: 0,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
it("Solar production, consuming some and returning the remainder to grid.", () => {
|
||||
[2.99, 3, 10, 100].forEach((s) => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 0,
|
||||
to_grid: 3,
|
||||
solar: s,
|
||||
to_battery: undefined,
|
||||
from_battery: undefined,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
used_solar: Math.max(0, s - 3),
|
||||
used_grid: 0,
|
||||
used_battery: 0,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: 3,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
it("Solar production with simultaneous grid consumption. Excess solar returned to the grid.", () => {
|
||||
[
|
||||
[0, 0],
|
||||
[3, 0],
|
||||
[0, 3],
|
||||
[5, 4],
|
||||
[4, 5],
|
||||
[10, 3],
|
||||
[3, 7],
|
||||
[3, 7.1],
|
||||
].forEach(([from_grid, to_grid]) => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid,
|
||||
to_grid,
|
||||
solar: 7,
|
||||
to_battery: undefined,
|
||||
from_battery: undefined,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
used_solar: Math.max(0, 7 - to_grid),
|
||||
used_grid: from_grid,
|
||||
used_battery: 0,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: to_grid,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
it("Charging the battery from the grid", () => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 0,
|
||||
solar: 0,
|
||||
to_battery: 3,
|
||||
from_battery: 0,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 3,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 0,
|
||||
used_grid: 2,
|
||||
used_battery: 0,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
it("Consuming from the grid and battery simultaneously", () => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 0,
|
||||
solar: 0,
|
||||
to_battery: 0,
|
||||
from_battery: 5,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 0,
|
||||
used_grid: 5,
|
||||
used_battery: 5,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
it("Consuming some battery and returning some battery to the grid", () => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 0,
|
||||
to_grid: 4,
|
||||
solar: 0,
|
||||
to_battery: 0,
|
||||
from_battery: 5,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 4,
|
||||
used_solar: 0,
|
||||
used_grid: 0,
|
||||
used_battery: 1,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
/* Fails
|
||||
it("Charging and discharging the battery to/from the grid in the same interval.", () => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 1,
|
||||
solar: 0,
|
||||
to_battery: 3,
|
||||
from_battery: 1,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 3,
|
||||
battery_to_grid: 1,
|
||||
used_solar: 0,
|
||||
used_grid: 1,
|
||||
used_battery: 0,
|
||||
}
|
||||
);
|
||||
}); */
|
||||
/* Test does not pass, battery is not really correct when solar is not present
|
||||
it("Charging the battery with no solar sensor.", () => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 0,
|
||||
solar: undefined,
|
||||
to_battery: 3,
|
||||
from_battery: 0,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 3,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 0,
|
||||
used_grid: 2,
|
||||
used_battery: 0,
|
||||
}
|
||||
);
|
||||
}); */
|
||||
/* Test does not pass
|
||||
it("Discharging battery to grid while also consuming from grid.", () => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 4,
|
||||
solar: 0,
|
||||
to_battery: 0,
|
||||
from_battery: 4,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 4,
|
||||
used_solar: 0,
|
||||
used_grid: 5,
|
||||
used_battery: 0,
|
||||
}
|
||||
);
|
||||
});
|
||||
*/
|
||||
|
||||
it("Grid, solar, and battery", () => {
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 3,
|
||||
solar: 7,
|
||||
to_battery: 3,
|
||||
from_battery: 0,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 1,
|
||||
used_grid: 5,
|
||||
used_battery: 0,
|
||||
solar_to_battery: 3,
|
||||
solar_to_grid: 3,
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 3,
|
||||
solar: 7,
|
||||
to_battery: 3,
|
||||
from_battery: 10,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 1,
|
||||
used_grid: 5,
|
||||
used_battery: 10,
|
||||
solar_to_battery: 3,
|
||||
solar_to_grid: 3,
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 2,
|
||||
to_grid: 7,
|
||||
solar: 7,
|
||||
to_battery: 1,
|
||||
from_battery: 1,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 1,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 0,
|
||||
used_grid: 1,
|
||||
used_battery: 1,
|
||||
solar_to_battery: 0,
|
||||
solar_to_grid: 7,
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 2,
|
||||
to_grid: 7,
|
||||
solar: 9,
|
||||
to_battery: 1,
|
||||
from_battery: 1,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 0,
|
||||
used_solar: 1,
|
||||
used_grid: 2,
|
||||
used_battery: 1,
|
||||
solar_to_battery: 1,
|
||||
solar_to_grid: 7,
|
||||
}
|
||||
);
|
||||
/* Test does not pass
|
||||
assert.deepEqual(
|
||||
computeConsumptionSingle({
|
||||
from_grid: 5,
|
||||
to_grid: 3,
|
||||
solar: 1,
|
||||
to_battery: 0,
|
||||
from_battery: 2,
|
||||
}),
|
||||
{
|
||||
grid_to_battery: 0,
|
||||
battery_to_grid: 2,
|
||||
used_solar: 0,
|
||||
used_grid: 5,
|
||||
used_battery: 0,
|
||||
}
|
||||
);
|
||||
*/
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user