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;
|
from_battery?: number;
|
||||||
solar?: number;
|
solar?: number;
|
||||||
};
|
};
|
||||||
|
timestamps: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EnergyConsumptionData {
|
export interface EnergyConsumptionData {
|
||||||
total: Record<number, number>;
|
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(
|
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]) => {
|
Object.entries(statIds).forEach(([key, subStatIds]) => {
|
||||||
const totalStats: Record<number, number> = {};
|
const totalStats: Record<number, number> = {};
|
||||||
const sets: Record<string, Record<number, number>> = {};
|
const sets: Record<string, Record<number, number>> = {};
|
||||||
@ -886,6 +905,7 @@ const getSummedDataPartial = (
|
|||||||
sum += val;
|
sum += val;
|
||||||
totalStats[stat.start] =
|
totalStats[stat.start] =
|
||||||
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
stat.start in totalStats ? totalStats[stat.start] + val : val;
|
||||||
|
timestamps.add(stat.start);
|
||||||
});
|
});
|
||||||
sets[id] = set;
|
sets[id] = set;
|
||||||
});
|
});
|
||||||
@ -893,6 +913,8 @@ const getSummedDataPartial = (
|
|||||||
summedData.total[key] = sum;
|
summedData.total[key] = sum;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
summedData.timestamps = Array.from(timestamps).sort();
|
||||||
|
|
||||||
return summedData;
|
return summedData;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -915,25 +937,142 @@ export const computeConsumptionData = memoizeOne(
|
|||||||
const computeConsumptionDataPartial = (
|
const computeConsumptionDataPartial = (
|
||||||
data: EnergySumData
|
data: EnergySumData
|
||||||
): EnergyConsumptionData => {
|
): 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) => {
|
data.timestamps.forEach((t) => {
|
||||||
Object.keys(data[type]).forEach((start) => {
|
const used_total =
|
||||||
if (outData.total[start] === undefined) {
|
(data.from_grid?.[t] || 0) +
|
||||||
const consumption =
|
(data.solar?.[t] || 0) +
|
||||||
(data.from_grid?.[start] || 0) +
|
(data.from_battery?.[t] || 0) -
|
||||||
(data.solar?.[start] || 0) +
|
(data.to_grid?.[t] || 0) -
|
||||||
(data.from_battery?.[start] || 0) -
|
(data.to_battery?.[t] || 0);
|
||||||
(data.to_grid?.[start] || 0) -
|
|
||||||
(data.to_battery?.[start] || 0);
|
outData.used_total[t] = used_total;
|
||||||
outData.total[start] = consumption;
|
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;
|
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 = (
|
export const formatConsumptionShort = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
consumption: number | null,
|
consumption: number | null,
|
||||||
|
@ -347,12 +347,13 @@ export class HuiEnergyDevicesDetailGraphCard
|
|||||||
);
|
);
|
||||||
|
|
||||||
const untrackedConsumption: BarSeriesOption["data"] = [];
|
const untrackedConsumption: BarSeriesOption["data"] = [];
|
||||||
Object.keys(consumptionData.total)
|
Object.keys(consumptionData.used_total)
|
||||||
.sort((a, b) => Number(a) - Number(b))
|
.sort((a, b) => Number(a) - Number(b))
|
||||||
.forEach((time) => {
|
.forEach((time) => {
|
||||||
const ts = Number(time);
|
const ts = Number(time);
|
||||||
const value =
|
const value =
|
||||||
consumptionData.total[time] - (totalDeviceConsumption[time] || 0);
|
consumptionData.used_total[time] -
|
||||||
|
(totalDeviceConsumption[time] || 0);
|
||||||
const dataPoint: number[] = [ts, value];
|
const dataPoint: number[] = [ts, value];
|
||||||
if (compare) {
|
if (compare) {
|
||||||
dataPoint[2] = dataPoint[0];
|
dataPoint[2] = dataPoint[0];
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
getEnergyWaterUnit,
|
getEnergyWaterUnit,
|
||||||
formatConsumptionShort,
|
formatConsumptionShort,
|
||||||
getSummedData,
|
getSummedData,
|
||||||
|
computeConsumptionData,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
|
import { calculateStatisticsSumGrowth } from "../../../../data/recorder";
|
||||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
@ -111,6 +112,10 @@ class HuiEnergyDistrubutionCard
|
|||||||
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
const hasReturnToGrid = hasConsumption && types.grid![0].flow_to.length > 0;
|
||||||
|
|
||||||
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
||||||
|
const { consumption, compareConsumption: __ } = computeConsumptionData(
|
||||||
|
summedData,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
const totalFromGrid = summedData.total.from_grid ?? 0;
|
const totalFromGrid = summedData.total.from_grid ?? 0;
|
||||||
|
|
||||||
@ -154,55 +159,32 @@ class HuiEnergyDistrubutionCard
|
|||||||
|
|
||||||
let solarConsumption: number | null = null;
|
let solarConsumption: number | null = null;
|
||||||
if (hasSolarProduction) {
|
if (hasSolarProduction) {
|
||||||
solarConsumption =
|
solarConsumption = consumption.total.used_solar;
|
||||||
(totalSolarProduction || 0) -
|
|
||||||
(returnedToGrid || 0) -
|
|
||||||
(totalBatteryIn || 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let batteryFromGrid: null | number = null;
|
let batteryFromGrid: null | number = null;
|
||||||
let batteryToGrid: null | number = null;
|
let batteryToGrid: null | number = null;
|
||||||
if (solarConsumption !== null && solarConsumption < 0) {
|
if (hasBattery) {
|
||||||
// What we returned to the grid and what went in to the battery is more than produced,
|
batteryToGrid = consumption.total.battery_to_grid;
|
||||||
// so we have used grid energy to fill the battery
|
batteryFromGrid = consumption.total.grid_to_battery;
|
||||||
// or returned battery energy to the grid
|
|
||||||
if (hasBattery) {
|
|
||||||
batteryFromGrid = solarConsumption * -1;
|
|
||||||
if (batteryFromGrid > totalFromGrid) {
|
|
||||||
batteryToGrid = batteryFromGrid - totalFromGrid;
|
|
||||||
batteryFromGrid = totalFromGrid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
solarConsumption = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let solarToBattery: null | number = null;
|
let solarToBattery: null | number = null;
|
||||||
|
let solarToGrid: null | number = null;
|
||||||
|
if (hasSolarProduction) {
|
||||||
|
solarToGrid = consumption.total.solar_to_grid;
|
||||||
|
}
|
||||||
if (hasSolarProduction && hasBattery) {
|
if (hasSolarProduction && hasBattery) {
|
||||||
if (!batteryToGrid) {
|
solarToBattery = consumption.total.solar_to_battery;
|
||||||
batteryToGrid = Math.max(
|
|
||||||
0,
|
|
||||||
(returnedToGrid || 0) -
|
|
||||||
(totalSolarProduction || 0) -
|
|
||||||
(totalBatteryIn || 0) -
|
|
||||||
(batteryFromGrid || 0)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
solarToBattery = totalBatteryIn! - (batteryFromGrid || 0);
|
|
||||||
} else if (!hasSolarProduction && hasBattery) {
|
|
||||||
batteryToGrid = returnedToGrid;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let batteryConsumption: number | null = null;
|
let batteryConsumption: number | null = null;
|
||||||
if (hasBattery) {
|
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(
|
const totalHomeConsumption = Math.max(0, consumption.total.used_total);
|
||||||
0,
|
|
||||||
gridConsumption + (solarConsumption || 0) + (batteryConsumption || 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
let homeSolarCircumference: number | undefined;
|
let homeSolarCircumference: number | undefined;
|
||||||
if (hasSolarProduction) {
|
if (hasSolarProduction) {
|
||||||
@ -262,7 +244,7 @@ class HuiEnergyDistrubutionCard
|
|||||||
const totalLines =
|
const totalLines =
|
||||||
gridConsumption +
|
gridConsumption +
|
||||||
(solarConsumption || 0) +
|
(solarConsumption || 0) +
|
||||||
(returnedToGrid ? returnedToGrid - (batteryToGrid || 0) : 0) +
|
(solarToGrid || 0) +
|
||||||
(solarToBattery || 0) +
|
(solarToBattery || 0) +
|
||||||
(batteryConsumption || 0) +
|
(batteryConsumption || 0) +
|
||||||
(batteryFromGrid || 0) +
|
(batteryFromGrid || 0) +
|
||||||
@ -662,18 +644,14 @@ class HuiEnergyDistrubutionCard
|
|||||||
d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100"
|
d="M0,${hasBattery ? 50 : hasSolarProduction ? 56 : 53} H100"
|
||||||
vector-effect="non-scaling-stroke"
|
vector-effect="non-scaling-stroke"
|
||||||
></path>
|
></path>
|
||||||
${returnedToGrid && hasSolarProduction && this._animate
|
${solarToGrid && this._animate
|
||||||
? svg`<circle
|
? svg`<circle
|
||||||
r="1"
|
r="1"
|
||||||
class="return"
|
class="return"
|
||||||
vector-effect="non-scaling-stroke"
|
vector-effect="non-scaling-stroke"
|
||||||
>
|
>
|
||||||
<animateMotion
|
<animateMotion
|
||||||
dur="${
|
dur="${6 - (solarToGrid / totalLines) * 6}s"
|
||||||
6 -
|
|
||||||
((returnedToGrid - (batteryToGrid || 0)) / totalLines) *
|
|
||||||
6
|
|
||||||
}s"
|
|
||||||
repeatCount="indefinite"
|
repeatCount="indefinite"
|
||||||
calcMode="linear"
|
calcMode="linear"
|
||||||
>
|
>
|
||||||
|
@ -10,6 +10,7 @@ import "../../../../components/ha-svg-icon";
|
|||||||
import "../../../../components/ha-tooltip";
|
import "../../../../components/ha-tooltip";
|
||||||
import type { EnergyData } from "../../../../data/energy";
|
import type { EnergyData } from "../../../../data/energy";
|
||||||
import {
|
import {
|
||||||
|
computeConsumptionData,
|
||||||
getEnergyDataCollection,
|
getEnergyDataCollection,
|
||||||
getSummedData,
|
getSummedData,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
@ -76,70 +77,14 @@ class HuiEnergySelfSufficiencyGaugeCard
|
|||||||
|
|
||||||
// The strategy only includes this card if we have a grid.
|
// The strategy only includes this card if we have a grid.
|
||||||
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
const { summedData, compareSummedData: _ } = getSummedData(this._data);
|
||||||
|
const { consumption, compareConsumption: __ } = computeConsumptionData(
|
||||||
const hasSolarProduction = summedData.solar !== undefined;
|
summedData,
|
||||||
const hasBattery =
|
undefined
|
||||||
summedData.to_battery !== undefined ||
|
);
|
||||||
summedData.from_battery !== undefined;
|
|
||||||
const hasReturnToGrid = summedData.to_grid !== undefined;
|
|
||||||
|
|
||||||
const totalFromGrid = summedData.total.from_grid ?? 0;
|
const totalFromGrid = summedData.total.from_grid ?? 0;
|
||||||
|
|
||||||
let totalSolarProduction: number | null = null;
|
const totalHomeConsumption = Math.max(0, consumption.total.used_total);
|
||||||
|
|
||||||
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)
|
|
||||||
);
|
|
||||||
|
|
||||||
let value: number | undefined;
|
let value: number | undefined;
|
||||||
if (
|
if (
|
||||||
@ -147,7 +92,7 @@ class HuiEnergySelfSufficiencyGaugeCard
|
|||||||
totalHomeConsumption !== null &&
|
totalHomeConsumption !== null &&
|
||||||
totalHomeConsumption > 0
|
totalHomeConsumption > 0
|
||||||
) {
|
) {
|
||||||
value = (1 - totalFromGrid / totalHomeConsumption) * 100;
|
value = (1 - Math.min(1, totalFromGrid / totalHomeConsumption)) * 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
@ -14,8 +14,13 @@ import { getEnergyColor } from "./common/color";
|
|||||||
import { formatNumber } from "../../../../common/number/format_number";
|
import { formatNumber } from "../../../../common/number/format_number";
|
||||||
import "../../../../components/chart/ha-chart-base";
|
import "../../../../components/chart/ha-chart-base";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
import type { EnergyData, EnergySumData } from "../../../../data/energy";
|
import type {
|
||||||
|
EnergyData,
|
||||||
|
EnergySumData,
|
||||||
|
EnergyConsumptionData,
|
||||||
|
} from "../../../../data/energy";
|
||||||
import {
|
import {
|
||||||
|
computeConsumptionData,
|
||||||
getEnergyDataCollection,
|
getEnergyDataCollection,
|
||||||
getSummedData,
|
getSummedData,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
@ -283,6 +288,10 @@ export class HuiEnergyUsageGraphCard
|
|||||||
this._compareEnd = energyData.endCompare;
|
this._compareEnd = energyData.endCompare;
|
||||||
|
|
||||||
const { summedData, compareSummedData } = getSummedData(energyData);
|
const { summedData, compareSummedData } = getSummedData(energyData);
|
||||||
|
const { consumption, compareConsumption } = computeConsumptionData(
|
||||||
|
summedData,
|
||||||
|
compareSummedData
|
||||||
|
);
|
||||||
|
|
||||||
if (energyData.statsCompare) {
|
if (energyData.statsCompare) {
|
||||||
datasets.push(
|
datasets.push(
|
||||||
@ -290,6 +299,7 @@ export class HuiEnergyUsageGraphCard
|
|||||||
energyData.statsCompare,
|
energyData.statsCompare,
|
||||||
energyData.statsMetadata,
|
energyData.statsMetadata,
|
||||||
compareSummedData!,
|
compareSummedData!,
|
||||||
|
compareConsumption!,
|
||||||
statIds,
|
statIds,
|
||||||
colorIndices,
|
colorIndices,
|
||||||
computedStyles,
|
computedStyles,
|
||||||
@ -315,6 +325,7 @@ export class HuiEnergyUsageGraphCard
|
|||||||
energyData.stats,
|
energyData.stats,
|
||||||
energyData.statsMetadata,
|
energyData.statsMetadata,
|
||||||
summedData,
|
summedData,
|
||||||
|
consumption,
|
||||||
statIds,
|
statIds,
|
||||||
colorIndices,
|
colorIndices,
|
||||||
computedStyles,
|
computedStyles,
|
||||||
@ -333,6 +344,7 @@ export class HuiEnergyUsageGraphCard
|
|||||||
statistics: Statistics,
|
statistics: Statistics,
|
||||||
statisticsMetaData: Record<string, StatisticsMetaData>,
|
statisticsMetaData: Record<string, StatisticsMetaData>,
|
||||||
summedData: EnergySumData,
|
summedData: EnergySumData,
|
||||||
|
consumptionData: EnergyConsumptionData,
|
||||||
statIdsByCat: {
|
statIdsByCat: {
|
||||||
to_grid?: string[] | undefined;
|
to_grid?: string[] | undefined;
|
||||||
from_grid?: string[] | undefined;
|
from_grid?: string[] | undefined;
|
||||||
@ -361,8 +373,7 @@ export class HuiEnergyUsageGraphCard
|
|||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
Object.entries(statIdsByCat).forEach(([key, statIds]) => {
|
Object.entries(statIdsByCat).forEach(([key, statIds]) => {
|
||||||
const add = !["solar", "from_battery"].includes(key);
|
if (!["to_grid", "from_grid", "to_battery"].includes(key)) {
|
||||||
if (!add) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sets: Record<string, Record<number, number>> = {};
|
const sets: Record<string, Record<number, number>> = {};
|
||||||
@ -387,47 +398,22 @@ export class HuiEnergyUsageGraphCard
|
|||||||
combinedData[key] = sets;
|
combinedData[key] = sets;
|
||||||
});
|
});
|
||||||
|
|
||||||
const grid_to_battery = {};
|
combinedData.used_solar = { used_solar: consumptionData.used_solar };
|
||||||
const battery_to_grid = {};
|
combinedData.used_battery = {
|
||||||
if ((summedData.to_grid || summedData.to_battery) && summedData.solar) {
|
used_battery: consumptionData.used_battery,
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combinedData.from_grid && summedData.to_battery) {
|
if (combinedData.from_grid && summedData.to_battery) {
|
||||||
const used_grid = {};
|
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 noOfSources = 0;
|
||||||
let source: string;
|
let source: string;
|
||||||
for (const [key, stats] of Object.entries(combinedData.from_grid)) {
|
for (const [key, stats] of Object.entries(combinedData.from_grid)) {
|
||||||
@ -440,30 +426,19 @@ export class HuiEnergyUsageGraphCard
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (noOfSources === 1) {
|
if (noOfSources === 1) {
|
||||||
combinedData.from_grid[source!][start] -= grid_to_battery[start] || 0;
|
combinedData.from_grid[source!][start] =
|
||||||
|
consumptionData.used_grid[start];
|
||||||
} else {
|
} else {
|
||||||
let total_from_grid = 0;
|
|
||||||
Object.values(combinedData.from_grid).forEach((stats) => {
|
Object.values(combinedData.from_grid).forEach((stats) => {
|
||||||
total_from_grid += stats[start] || 0;
|
|
||||||
delete stats[start];
|
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 };
|
combinedData.used_grid = { used_grid };
|
||||||
}
|
}
|
||||||
|
|
||||||
let allKeys: string[] = [];
|
const uniqueKeys = summedData.timestamps;
|
||||||
|
|
||||||
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 compareTransform = getCompareTransform(
|
const compareTransform = getCompareTransform(
|
||||||
this._start,
|
this._start,
|
||||||
@ -477,14 +452,14 @@ export class HuiEnergyUsageGraphCard
|
|||||||
for (const key of uniqueKeys) {
|
for (const key of uniqueKeys) {
|
||||||
const value = source[key] || 0;
|
const value = source[key] || 0;
|
||||||
const dataPoint = [
|
const dataPoint = [
|
||||||
Number(key),
|
new Date(key),
|
||||||
value && ["to_grid", "to_battery"].includes(type)
|
value && ["to_grid", "to_battery"].includes(type)
|
||||||
? -1 * value
|
? -1 * value
|
||||||
: value,
|
: value,
|
||||||
];
|
];
|
||||||
if (compare) {
|
if (compare) {
|
||||||
dataPoint[2] = dataPoint[0];
|
dataPoint[2] = dataPoint[0];
|
||||||
dataPoint[0] = compareTransform(dataPoint[0]);
|
dataPoint[0] = compareTransform(dataPoint[0] as Date);
|
||||||
}
|
}
|
||||||
points.push(dataPoint);
|
points.push(dataPoint);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,10 @@ import {
|
|||||||
DateFormat,
|
DateFormat,
|
||||||
TimeZone,
|
TimeZone,
|
||||||
} from "../../src/data/translation";
|
} from "../../src/data/translation";
|
||||||
import { formatConsumptionShort } from "../../src/data/energy";
|
import {
|
||||||
|
computeConsumptionSingle,
|
||||||
|
formatConsumptionShort,
|
||||||
|
} from "../../src/data/energy";
|
||||||
import type { HomeAssistant } from "../../src/types";
|
import type { HomeAssistant } from "../../src/types";
|
||||||
|
|
||||||
describe("Energy Short Format Test", () => {
|
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