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:
karwosts 2025-04-29 20:57:26 -07:00 committed by GitHub
parent bc582db7fc
commit 92353ebed5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 510 additions and 180 deletions

View File

@ -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,

View File

@ -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];

View File

@ -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) {
// 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) { if (hasBattery) {
batteryFromGrid = solarConsumption * -1; batteryToGrid = consumption.total.battery_to_grid;
if (batteryFromGrid > totalFromGrid) { batteryFromGrid = consumption.total.grid_to_battery;
batteryToGrid = batteryFromGrid - totalFromGrid;
batteryFromGrid = totalFromGrid;
}
}
solarConsumption = 0;
} }
let solarToBattery: null | number = null; let solarToBattery: null | number = null;
if (hasSolarProduction && hasBattery) { let solarToGrid: null | number = null;
if (!batteryToGrid) { if (hasSolarProduction) {
batteryToGrid = Math.max( solarToGrid = consumption.total.solar_to_grid;
0,
(returnedToGrid || 0) -
(totalSolarProduction || 0) -
(totalBatteryIn || 0) -
(batteryFromGrid || 0)
);
} }
solarToBattery = totalBatteryIn! - (batteryFromGrid || 0); if (hasSolarProduction && hasBattery) {
} else if (!hasSolarProduction && hasBattery) { solarToBattery = consumption.total.solar_to_battery;
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"
> >

View File

@ -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`

View File

@ -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);
} }

View File

@ -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,
}
);
*/
});
});