From 4d330fba8a1465393a0aa1db8808ec57dd69a289 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 28 Jul 2021 09:20:06 -0700 Subject: [PATCH] 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 --- src/data/history.ts | 125 ++++++++++++++++++ .../hui-energy-carbon-consumed-gauge-card.ts | 40 ++++-- .../energy/hui-energy-distribution-card.ts | 101 +++++++------- test-mocha/data/history.spec.ts | 77 +++++++++++ 4 files changed, 278 insertions(+), 65 deletions(-) create mode 100644 test-mocha/data/history.spec.ts diff --git a/src/data/history.ts b/src/data/history.ts index a111a196b7..481ddf7792 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -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; +}; diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index 4885489c47..a54f12c88c 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -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 { `; } - 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 { + 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, diff --git a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts index 9debef5f18..78f1af3b55 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -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 { 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 { 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") { diff --git a/test-mocha/data/history.spec.ts b/test-mocha/data/history.spec.ts new file mode 100644 index 0000000000..8ba9efd651 --- /dev/null +++ b/test-mocha/data/history.spec.ts @@ -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 + ); + }); +});