mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
New solar-consumed-gauge algorithm (#25326)
* New solar-consumed-gauge algorithm * update tests * Apply suggestion * reduce duplicate code
This commit is contained in:
parent
6d931b9e37
commit
821a0bc418
@ -1118,3 +1118,88 @@ export const formatConsumptionShort = (
|
||||
pickedUnit
|
||||
);
|
||||
};
|
||||
|
||||
export const calculateSolarConsumedGauge = (
|
||||
hasBattery: boolean,
|
||||
data: EnergySumData
|
||||
): number | undefined => {
|
||||
if (!data.total.solar) {
|
||||
return undefined;
|
||||
}
|
||||
const { consumption, compareConsumption: _ } = computeConsumptionData(
|
||||
data,
|
||||
undefined
|
||||
);
|
||||
if (!hasBattery) {
|
||||
const solarProduction = data.total.solar;
|
||||
return (consumption.total.used_solar / solarProduction) * 100;
|
||||
}
|
||||
|
||||
let solarConsumed = 0;
|
||||
let solarReturned = 0;
|
||||
const batteryLifo: { type: "solar" | "grid"; value: number }[] = [];
|
||||
|
||||
// Here we will attempt to track consumed solar energy, as it routes through the battery and ultimately to consumption or grid.
|
||||
// At each timestamp we will track energy added to the battery (and its source), and we will drain this in Last-in/First-out order.
|
||||
// Energy leaving the battery when the stack is empty will just be ignored, as we cannot determine where it came from.
|
||||
// This is likely energy stored during a previous period.
|
||||
|
||||
data.timestamps.forEach((t) => {
|
||||
solarConsumed += consumption.used_solar[t] ?? 0;
|
||||
solarReturned += consumption.solar_to_grid[t] ?? 0;
|
||||
|
||||
if (consumption.grid_to_battery[t]) {
|
||||
batteryLifo.push({
|
||||
type: "grid",
|
||||
value: consumption.grid_to_battery[t],
|
||||
});
|
||||
}
|
||||
if (consumption.solar_to_battery[t]) {
|
||||
batteryLifo.push({
|
||||
type: "solar",
|
||||
value: consumption.solar_to_battery[t],
|
||||
});
|
||||
}
|
||||
|
||||
let batteryToGrid = consumption.battery_to_grid[t] ?? 0;
|
||||
let usedBattery = consumption.used_battery[t] ?? 0;
|
||||
|
||||
const drainBattery = function (amount: number): {
|
||||
energy: number;
|
||||
type: "solar" | "grid";
|
||||
} {
|
||||
const lastLifo = batteryLifo[batteryLifo.length - 1];
|
||||
const type = lastLifo.type;
|
||||
if (amount >= lastLifo.value) {
|
||||
const energy = lastLifo.value;
|
||||
batteryLifo.pop();
|
||||
return { energy, type };
|
||||
}
|
||||
lastLifo.value -= amount;
|
||||
return { energy: amount, type };
|
||||
};
|
||||
|
||||
while (usedBattery > 0 && batteryLifo.length) {
|
||||
const { energy, type } = drainBattery(usedBattery);
|
||||
if (type === "solar") {
|
||||
solarConsumed += energy;
|
||||
}
|
||||
usedBattery -= energy;
|
||||
}
|
||||
|
||||
while (batteryToGrid > 0 && batteryLifo.length) {
|
||||
const { energy, type } = drainBattery(batteryToGrid);
|
||||
if (type === "solar") {
|
||||
solarReturned += energy;
|
||||
}
|
||||
batteryToGrid -= energy;
|
||||
}
|
||||
});
|
||||
|
||||
const totalProduction = solarConsumed + solarReturned;
|
||||
const hasSolarProduction = !!totalProduction;
|
||||
if (hasSolarProduction) {
|
||||
return (solarConsumed / totalProduction) * 100;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import "../../../../components/ha-gauge";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
calculateSolarConsumedGauge,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
} from "../../../../data/energy";
|
||||
@ -78,18 +79,12 @@ class HuiEnergySolarGaugeCard
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const totalSolarProduction = summedData.total.solar;
|
||||
|
||||
const productionReturnedToGrid = summedData.total.to_grid ?? null;
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
if (productionReturnedToGrid !== null && totalSolarProduction) {
|
||||
const consumedSolar = Math.max(
|
||||
0,
|
||||
totalSolarProduction - productionReturnedToGrid
|
||||
);
|
||||
value = (consumedSolar / totalSolarProduction) * 100;
|
||||
if (productionReturnedToGrid !== null) {
|
||||
const hasBattery = !!summedData.to_battery || !!summedData.from_battery;
|
||||
value = calculateSolarConsumedGauge(hasBattery, summedData);
|
||||
}
|
||||
|
||||
return html`
|
||||
@ -125,7 +120,7 @@ class HuiEnergySolarGaugeCard
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: totalSolarProduction === 0
|
||||
: productionReturnedToGrid !== null
|
||||
? this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.solar_consumed_gauge.not_produced_solar_energy"
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ import {
|
||||
import {
|
||||
computeConsumptionSingle,
|
||||
formatConsumptionShort,
|
||||
calculateSolarConsumedGauge,
|
||||
} from "../../src/data/energy";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
@ -506,3 +507,266 @@ describe("Energy Usage Calculation Tests", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Self-consumed solar gauge tests", () => {
|
||||
it("no battery", () => {
|
||||
const hasBattery = false;
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
total: {},
|
||||
timestamps: [0],
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 0,
|
||||
},
|
||||
total: {
|
||||
solar: 0,
|
||||
},
|
||||
timestamps: [0],
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 1,
|
||||
"1": 3,
|
||||
},
|
||||
total: {
|
||||
solar: 4,
|
||||
},
|
||||
timestamps: [0, 1],
|
||||
}),
|
||||
100
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 1,
|
||||
"1": 3,
|
||||
},
|
||||
to_grid: {
|
||||
"1": 1,
|
||||
},
|
||||
total: {
|
||||
solar: 4,
|
||||
to_grid: 1,
|
||||
},
|
||||
timestamps: [0, 1],
|
||||
}),
|
||||
75
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 1,
|
||||
"1": 3,
|
||||
},
|
||||
to_grid: {
|
||||
"0": 1,
|
||||
"1": 3,
|
||||
},
|
||||
total: {
|
||||
solar: 4,
|
||||
to_grid: 4,
|
||||
},
|
||||
timestamps: [0, 1],
|
||||
}),
|
||||
0
|
||||
);
|
||||
});
|
||||
it("with battery", () => {
|
||||
const hasBattery = true;
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
total: {},
|
||||
timestamps: [0],
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 0,
|
||||
},
|
||||
total: {
|
||||
solar: 0,
|
||||
},
|
||||
timestamps: [0],
|
||||
}),
|
||||
undefined
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 1,
|
||||
"1": 3,
|
||||
},
|
||||
total: {
|
||||
solar: 4,
|
||||
},
|
||||
timestamps: [0, 1],
|
||||
}),
|
||||
100
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 1,
|
||||
"1": 3,
|
||||
},
|
||||
to_grid: {
|
||||
"1": 1,
|
||||
},
|
||||
total: {
|
||||
solar: 4,
|
||||
},
|
||||
timestamps: [0, 1],
|
||||
}),
|
||||
75
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"10": 1,
|
||||
},
|
||||
to_grid: {
|
||||
"0": 1,
|
||||
"1": 1,
|
||||
"2": 1,
|
||||
"3": 1,
|
||||
},
|
||||
from_battery: {
|
||||
"0": 1,
|
||||
"1": 1,
|
||||
"2": 1,
|
||||
"3": 1,
|
||||
},
|
||||
total: {
|
||||
solar: 1,
|
||||
},
|
||||
timestamps: [0, 1, 2, 3, 10],
|
||||
}),
|
||||
// As the battery is discharged from unknown source, it does not affect solar production number.
|
||||
100
|
||||
);
|
||||
assert.deepEqual(
|
||||
calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 10,
|
||||
},
|
||||
to_battery: {
|
||||
"0": 10,
|
||||
},
|
||||
to_grid: {
|
||||
"1": 3,
|
||||
"3": 1,
|
||||
},
|
||||
from_battery: {
|
||||
"1": 3,
|
||||
"2": 2,
|
||||
"3": 2,
|
||||
"4": 3,
|
||||
"5": 100, // Unknown source, not counted
|
||||
},
|
||||
total: {
|
||||
solar: 10,
|
||||
},
|
||||
timestamps: [0, 1, 2, 3, 4, 5],
|
||||
}),
|
||||
// As the battery is discharged from unknown source, it does not affect solar production number.
|
||||
60
|
||||
);
|
||||
});
|
||||
it("complex battery/solar/grid", () => {
|
||||
const hasBattery = true;
|
||||
|
||||
const value = calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"1": 6,
|
||||
"2": 0,
|
||||
"3": 7,
|
||||
},
|
||||
to_battery: {
|
||||
"1": 5,
|
||||
"2": 5,
|
||||
"3": 7,
|
||||
},
|
||||
to_grid: {
|
||||
"0": 5,
|
||||
"10": 1,
|
||||
"11": 1,
|
||||
"12": 5,
|
||||
"13": 3,
|
||||
},
|
||||
from_grid: {
|
||||
"2": 5,
|
||||
},
|
||||
from_battery: {
|
||||
"0": 5,
|
||||
"10": 3,
|
||||
"11": 4,
|
||||
"12": 5,
|
||||
"13": 5,
|
||||
},
|
||||
total: {
|
||||
// Total is mostly don't care when hasBattery, only hourly values are used
|
||||
solar: 13,
|
||||
},
|
||||
timestamps: [0, 1, 2, 3, 10, 11, 12, 13],
|
||||
});
|
||||
// "1" - consumed 1 solar, 5 sent to battery
|
||||
// "10" - consumed 2/3 of solar energy stored in battery
|
||||
// "11" - consumed 3/4 of solar energy stored in battery
|
||||
// "12" - skipped as this is energy from grid, not counted
|
||||
// "13" - consumed 2/5 of solar energy stored in battery
|
||||
const expectedNumerator = 1 + 2 + 3 + 0 + 2; // 8
|
||||
const expectedDenominator = 1 + 3 + 4 + 0 + 5; // 13
|
||||
assert.equal(
|
||||
Math.round(value!),
|
||||
Math.round((expectedNumerator / expectedDenominator) * 100)
|
||||
);
|
||||
});
|
||||
|
||||
it("complex battery/solar/grid #2", () => {
|
||||
const hasBattery = true;
|
||||
const value = calculateSolarConsumedGauge(hasBattery, {
|
||||
solar: {
|
||||
"0": 100,
|
||||
"2": 100,
|
||||
},
|
||||
to_battery: {
|
||||
"0": 100,
|
||||
"1": 100,
|
||||
"2": 100,
|
||||
},
|
||||
to_grid: {
|
||||
"10": 50,
|
||||
},
|
||||
from_grid: {
|
||||
"1": 100,
|
||||
},
|
||||
from_battery: {
|
||||
"10": 300,
|
||||
},
|
||||
total: {
|
||||
solar: 200,
|
||||
to_battery: 300,
|
||||
to_grid: 50,
|
||||
from_grid: 100,
|
||||
from_battery: 300,
|
||||
},
|
||||
timestamps: [0, 1, 2, 10],
|
||||
});
|
||||
const expectedNumerator = 200 - 50;
|
||||
const expectedDenominator = 200; // ignoring 100 from grid
|
||||
assert.equal(
|
||||
Math.round(value!),
|
||||
Math.round((expectedNumerator / expectedDenominator) * 100)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user