mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +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
|
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 "../../../../components/ha-svg-icon";
|
||||||
import type { EnergyData } from "../../../../data/energy";
|
import type { EnergyData } from "../../../../data/energy";
|
||||||
import {
|
import {
|
||||||
|
calculateSolarConsumedGauge,
|
||||||
getEnergyDataCollection,
|
getEnergyDataCollection,
|
||||||
getSummedData,
|
getSummedData,
|
||||||
} from "../../../../data/energy";
|
} from "../../../../data/energy";
|
||||||
@ -78,18 +79,12 @@ class HuiEnergySolarGaugeCard
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
const totalSolarProduction = summedData.total.solar;
|
|
||||||
|
|
||||||
const productionReturnedToGrid = summedData.total.to_grid ?? null;
|
const productionReturnedToGrid = summedData.total.to_grid ?? null;
|
||||||
|
|
||||||
let value: number | undefined;
|
let value: number | undefined;
|
||||||
|
if (productionReturnedToGrid !== null) {
|
||||||
if (productionReturnedToGrid !== null && totalSolarProduction) {
|
const hasBattery = !!summedData.to_battery || !!summedData.from_battery;
|
||||||
const consumedSolar = Math.max(
|
value = calculateSolarConsumedGauge(hasBattery, summedData);
|
||||||
0,
|
|
||||||
totalSolarProduction - productionReturnedToGrid
|
|
||||||
);
|
|
||||||
value = (consumedSolar / totalSolarProduction) * 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@ -125,7 +120,7 @@ class HuiEnergySolarGaugeCard
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: totalSolarProduction === 0
|
: productionReturnedToGrid !== null
|
||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
"ui.panel.lovelace.cards.energy.solar_consumed_gauge.not_produced_solar_energy"
|
"ui.panel.lovelace.cards.energy.solar_consumed_gauge.not_produced_solar_energy"
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
computeConsumptionSingle,
|
computeConsumptionSingle,
|
||||||
formatConsumptionShort,
|
formatConsumptionShort,
|
||||||
|
calculateSolarConsumedGauge,
|
||||||
} from "../../src/data/energy";
|
} from "../../src/data/energy";
|
||||||
import type { HomeAssistant } from "../../src/types";
|
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