mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
Use historic CO2 data into account (#9626)
* Use historic CO2 data into account * Also add to gauge * Apply suggestions from code review * Format Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
2e04a55d5c
commit
4d330fba8a
@ -345,3 +345,128 @@ export const statisticsHaveType = (
|
||||
stats: StatisticValue[],
|
||||
type: StatisticType
|
||||
) => stats.some((stat) => stat[type] !== null);
|
||||
|
||||
/**
|
||||
* Get the earliest start from a list of statistics.
|
||||
*/
|
||||
const getMinStatisticStart = (stats: StatisticValue[][]): string | null => {
|
||||
let earliestString: string | null = null;
|
||||
let earliestTime: Date | null = null;
|
||||
|
||||
for (const stat of stats) {
|
||||
if (stat.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const curTime = new Date(stat[0].start);
|
||||
|
||||
if (earliestString === null) {
|
||||
earliestString = stat[0].start;
|
||||
earliestTime = curTime;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (curTime < earliestTime!) {
|
||||
earliestString = stat[0].start;
|
||||
earliestTime = curTime;
|
||||
}
|
||||
}
|
||||
|
||||
return earliestString;
|
||||
};
|
||||
|
||||
// Merge multiple sum statistics into one
|
||||
const mergeSumStatistics = (stats: StatisticValue[][]) => {
|
||||
const result: { start: string; sum: number }[] = [];
|
||||
|
||||
const statsCopy: StatisticValue[][] = stats.map((stat) => [...stat]);
|
||||
|
||||
while (statsCopy.some((stat) => stat.length > 0)) {
|
||||
const earliestStart = getMinStatisticStart(statsCopy)!;
|
||||
|
||||
let sum = 0;
|
||||
|
||||
for (const stat of statsCopy) {
|
||||
if (stat.length === 0) {
|
||||
continue;
|
||||
}
|
||||
if (stat[0].start !== earliestStart) {
|
||||
continue;
|
||||
}
|
||||
const statVal = stat.shift()!;
|
||||
if (!statVal.sum) {
|
||||
continue;
|
||||
}
|
||||
sum += statVal.sum;
|
||||
}
|
||||
|
||||
result.push({
|
||||
start: earliestStart,
|
||||
sum,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the growth of a statistic over the given period while applying a
|
||||
* per-period percentage.
|
||||
*/
|
||||
export const calculateStatisticsSumGrowthWithPercentage = (
|
||||
percentageStat: StatisticValue[],
|
||||
sumStats: StatisticValue[][]
|
||||
): number | null => {
|
||||
let sum: number | null = null;
|
||||
|
||||
if (sumStats.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sumStatsToProcess = mergeSumStatistics(sumStats);
|
||||
const percentageStatToProcess = [...percentageStat];
|
||||
|
||||
let lastSum: number | null = null;
|
||||
|
||||
// pre-populate lastSum with last sum statistic _before_ the first percentage statistic
|
||||
for (const stat of sumStatsToProcess) {
|
||||
if (new Date(stat.start) >= new Date(percentageStat[0].start)) {
|
||||
break;
|
||||
}
|
||||
lastSum = stat.sum;
|
||||
}
|
||||
|
||||
while (percentageStatToProcess.length > 0) {
|
||||
if (!sumStatsToProcess.length) {
|
||||
return sum;
|
||||
}
|
||||
|
||||
// If they are not equal, pop the value that is earlier in time
|
||||
if (sumStatsToProcess[0].start !== percentageStatToProcess[0].start) {
|
||||
if (
|
||||
new Date(sumStatsToProcess[0].start) <
|
||||
new Date(percentageStatToProcess[0].start)
|
||||
) {
|
||||
sumStatsToProcess.shift();
|
||||
} else {
|
||||
percentageStatToProcess.shift();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const sumStatValue = sumStatsToProcess.shift()!;
|
||||
const percentageStatValue = percentageStatToProcess.shift()!;
|
||||
|
||||
if (lastSum !== null) {
|
||||
const sumGrowth = sumStatValue.sum! - lastSum;
|
||||
if (sum === null) {
|
||||
sum = sumGrowth * (percentageStatValue.mean! / 100);
|
||||
} else {
|
||||
sum += sumGrowth * (percentageStatValue.mean! / 100);
|
||||
}
|
||||
}
|
||||
|
||||
lastSum = sumStatValue.sum;
|
||||
}
|
||||
|
||||
return sum;
|
||||
};
|
||||
|
@ -10,6 +10,7 @@ import { energySourcesByType } from "../../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../../data/history";
|
||||
@ -42,7 +43,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
if (!this.hasUpdated) {
|
||||
this._getStatistics();
|
||||
this._fetchCO2SignalEntity();
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,12 +51,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._stats || this._co2SignalEntity === undefined) {
|
||||
return html`Loading...`;
|
||||
if (this._co2SignalEntity === null) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
if (!this._co2SignalEntity) {
|
||||
return html``;
|
||||
if (!this._stats || !this._co2SignalEntity) {
|
||||
return html`Loading...`;
|
||||
}
|
||||
|
||||
const co2State = this.hass.states[this._co2SignalEntity];
|
||||
@ -67,12 +67,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
</hui-warning>`;
|
||||
}
|
||||
|
||||
const co2percentage = Number(co2State.state);
|
||||
|
||||
if (isNaN(co2percentage)) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
const types = energySourcesByType(prefs);
|
||||
|
||||
@ -83,7 +77,17 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
|
||||
let value: number | undefined;
|
||||
|
||||
if (totalGridConsumption) {
|
||||
if (this._co2SignalEntity in this._stats && totalGridConsumption) {
|
||||
const highCarbonEnergy =
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
this._stats[this._co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map(
|
||||
(flow) => this._stats![flow.stat_energy_from]
|
||||
)
|
||||
.filter(Boolean)
|
||||
) || 0;
|
||||
|
||||
const totalSolarProduction = types.solar
|
||||
? calculateStatisticsSumGrowth(
|
||||
this._stats,
|
||||
@ -96,8 +100,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
|
||||
);
|
||||
|
||||
const highCarbonEnergy = (totalGridConsumption * co2percentage) / 100;
|
||||
|
||||
const totalEnergyConsumed =
|
||||
totalGridConsumption +
|
||||
(totalSolarProduction || 0) -
|
||||
@ -171,6 +173,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
await this._fetchCO2SignalEntity();
|
||||
|
||||
if (this._co2SignalEntity === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
@ -192,6 +200,10 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._co2SignalEntity) {
|
||||
statistics.push(this._co2SignalEntity);
|
||||
}
|
||||
|
||||
this._stats = await fetchStatistics(
|
||||
this.hass!,
|
||||
startDate,
|
||||
|
@ -18,6 +18,7 @@ import { energySourcesByType } from "../../../../data/energy";
|
||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||
import {
|
||||
calculateStatisticsSumGrowth,
|
||||
calculateStatisticsSumGrowthWithPercentage,
|
||||
fetchStatistics,
|
||||
Statistics,
|
||||
} from "../../../../data/history";
|
||||
@ -52,11 +53,9 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
||||
|
||||
if (!this._fetching && !this._stats) {
|
||||
this._fetching = true;
|
||||
Promise.all([this._getStatistics(), this._fetchCO2SignalEntity()]).then(
|
||||
() => {
|
||||
this._fetching = false;
|
||||
}
|
||||
);
|
||||
this._getStatistics().then(() => {
|
||||
this._fetching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -102,20 +101,6 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
||||
);
|
||||
}
|
||||
|
||||
// total consumption = consumption_from_grid + solar_production - return_to_grid
|
||||
|
||||
let co2percentage: number | undefined;
|
||||
|
||||
if (this._co2SignalEntity) {
|
||||
const co2State = this.hass.states[this._co2SignalEntity];
|
||||
if (co2State) {
|
||||
co2percentage = Number(co2State.state);
|
||||
if (isNaN(co2percentage)) {
|
||||
co2percentage = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const totalConsumption =
|
||||
totalGridConsumption +
|
||||
(totalSolarProduction || 0) -
|
||||
@ -133,22 +118,33 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
||||
|
||||
let homeLowCarbonCircumference: number | undefined;
|
||||
let homeHighCarbonCircumference: number | undefined;
|
||||
if (co2percentage !== undefined) {
|
||||
const gridPctHighCarbon = co2percentage / 100;
|
||||
|
||||
lowCarbonConsumption =
|
||||
totalGridConsumption - totalGridConsumption * gridPctHighCarbon;
|
||||
if (this._co2SignalEntity && this._co2SignalEntity in this._stats) {
|
||||
// Calculate high carbon consumption
|
||||
const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage(
|
||||
this._stats[this._co2SignalEntity],
|
||||
types
|
||||
.grid![0].flow_from.map((flow) => this._stats![flow.stat_energy_from])
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
const homePctGridHighCarbon =
|
||||
(gridPctHighCarbon * totalGridConsumption) / totalConsumption;
|
||||
if (highCarbonConsumption !== null) {
|
||||
const gridPctHighCarbon = highCarbonConsumption / totalConsumption;
|
||||
|
||||
homeHighCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE * homePctGridHighCarbon;
|
||||
lowCarbonConsumption =
|
||||
totalGridConsumption - totalGridConsumption * gridPctHighCarbon;
|
||||
|
||||
homeLowCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
(homeSolarCircumference || 0) -
|
||||
homeHighCarbonCircumference;
|
||||
const homePctGridHighCarbon =
|
||||
(gridPctHighCarbon * totalGridConsumption) / totalConsumption;
|
||||
|
||||
homeHighCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE * homePctGridHighCarbon;
|
||||
|
||||
homeLowCarbonCircumference =
|
||||
CIRCLE_CIRCUMFERENCE -
|
||||
(homeSolarCircumference || 0) -
|
||||
homeHighCarbonCircumference;
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
@ -362,7 +358,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchCO2SignalEntity() {
|
||||
private async _getStatistics(): Promise<void> {
|
||||
const [configEntries, entityRegistryEntries] = await Promise.all([
|
||||
getConfigEntries(this.hass),
|
||||
subscribeOne(this.hass.connection, subscribeEntityRegistry),
|
||||
@ -372,32 +368,35 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
||||
(entry) => entry.domain === "co2signal"
|
||||
);
|
||||
|
||||
if (!co2ConfigEntry) {
|
||||
return;
|
||||
this._co2SignalEntity = undefined;
|
||||
|
||||
if (co2ConfigEntry) {
|
||||
for (const entry of entityRegistryEntries) {
|
||||
if (entry.config_entry_id !== co2ConfigEntry.entry_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The integration offers 2 entities. We want the % one.
|
||||
const co2State = this.hass.states[entry.entity_id];
|
||||
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._co2SignalEntity = co2State.entity_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (const entry of entityRegistryEntries) {
|
||||
if (entry.config_entry_id !== co2ConfigEntry.entry_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The integration offers 2 entities. We want the % one.
|
||||
const co2State = this.hass.states[entry.entity_id];
|
||||
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
|
||||
continue;
|
||||
}
|
||||
|
||||
this._co2SignalEntity = co2State.entity_id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async _getStatistics(): Promise<void> {
|
||||
const startDate = new Date();
|
||||
startDate.setHours(0, 0, 0, 0);
|
||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||
|
||||
const statistics: string[] = [];
|
||||
|
||||
if (this._co2SignalEntity !== undefined) {
|
||||
statistics.push(this._co2SignalEntity);
|
||||
}
|
||||
|
||||
const prefs = this._config!.prefs;
|
||||
for (const source of prefs.energy_sources) {
|
||||
if (source.type === "solar") {
|
||||
|
77
test-mocha/data/history.spec.ts
Normal file
77
test-mocha/data/history.spec.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import { assert } from "chai";
|
||||
|
||||
import { calculateStatisticsSumGrowthWithPercentage } from "../../src/data/history";
|
||||
|
||||
describe("calculateStatisticsSumGrowthWithPercentage", () => {
|
||||
it("Returns null if not enough values", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage([], []),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it("Returns null if not enough values", async () => {
|
||||
assert.strictEqual(
|
||||
calculateStatisticsSumGrowthWithPercentage(
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: 75,
|
||||
mean: 50,
|
||||
min: 25,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.carbon_intensity",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: 100,
|
||||
mean: 75,
|
||||
min: 50,
|
||||
sum: null,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[
|
||||
[
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T04:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 50,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T05:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 100,
|
||||
state: null,
|
||||
},
|
||||
{
|
||||
statistic_id: "sensor.peak_consumption",
|
||||
start: "2021-07-28T07:00:00Z",
|
||||
last_reset: null,
|
||||
max: null,
|
||||
mean: null,
|
||||
min: null,
|
||||
sum: 200,
|
||||
state: null,
|
||||
},
|
||||
],
|
||||
[],
|
||||
]
|
||||
),
|
||||
100
|
||||
);
|
||||
});
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user