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:
Paulus Schoutsen 2021-07-28 09:20:06 -07:00 committed by GitHub
parent 2e04a55d5c
commit 4d330fba8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 278 additions and 65 deletions

View File

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

View File

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

View File

@ -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") {

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