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[],
|
stats: StatisticValue[],
|
||||||
type: StatisticType
|
type: StatisticType
|
||||||
) => stats.some((stat) => stat[type] !== null);
|
) => 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 { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
calculateStatisticsSumGrowth,
|
calculateStatisticsSumGrowth,
|
||||||
|
calculateStatisticsSumGrowthWithPercentage,
|
||||||
fetchStatistics,
|
fetchStatistics,
|
||||||
Statistics,
|
Statistics,
|
||||||
} from "../../../../data/history";
|
} from "../../../../data/history";
|
||||||
@ -42,7 +43,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
this._getStatistics();
|
this._getStatistics();
|
||||||
this._fetchCO2SignalEntity();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -51,12 +51,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._stats || this._co2SignalEntity === undefined) {
|
if (this._co2SignalEntity === null) {
|
||||||
return html`Loading...`;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._co2SignalEntity) {
|
if (!this._stats || !this._co2SignalEntity) {
|
||||||
return html``;
|
return html`Loading...`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const co2State = this.hass.states[this._co2SignalEntity];
|
const co2State = this.hass.states[this._co2SignalEntity];
|
||||||
@ -67,12 +67,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
</hui-warning>`;
|
</hui-warning>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const co2percentage = Number(co2State.state);
|
|
||||||
|
|
||||||
if (isNaN(co2percentage)) {
|
|
||||||
return html``;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prefs = this._config!.prefs;
|
const prefs = this._config!.prefs;
|
||||||
const types = energySourcesByType(prefs);
|
const types = energySourcesByType(prefs);
|
||||||
|
|
||||||
@ -83,7 +77,17 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
let value: number | undefined;
|
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
|
const totalSolarProduction = types.solar
|
||||||
? calculateStatisticsSumGrowth(
|
? calculateStatisticsSumGrowth(
|
||||||
this._stats,
|
this._stats,
|
||||||
@ -96,8 +100,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
|
types.grid![0].flow_to.map((flow) => flow.stat_energy_to)
|
||||||
);
|
);
|
||||||
|
|
||||||
const highCarbonEnergy = (totalGridConsumption * co2percentage) / 100;
|
|
||||||
|
|
||||||
const totalEnergyConsumed =
|
const totalEnergyConsumed =
|
||||||
totalGridConsumption +
|
totalGridConsumption +
|
||||||
(totalSolarProduction || 0) -
|
(totalSolarProduction || 0) -
|
||||||
@ -171,6 +173,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async _getStatistics(): Promise<void> {
|
private async _getStatistics(): Promise<void> {
|
||||||
|
await this._fetchCO2SignalEntity();
|
||||||
|
|
||||||
|
if (this._co2SignalEntity === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
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._stats = await fetchStatistics(
|
||||||
this.hass!,
|
this.hass!,
|
||||||
startDate,
|
startDate,
|
||||||
|
@ -18,6 +18,7 @@ import { energySourcesByType } from "../../../../data/energy";
|
|||||||
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
import { subscribeEntityRegistry } from "../../../../data/entity_registry";
|
||||||
import {
|
import {
|
||||||
calculateStatisticsSumGrowth,
|
calculateStatisticsSumGrowth,
|
||||||
|
calculateStatisticsSumGrowthWithPercentage,
|
||||||
fetchStatistics,
|
fetchStatistics,
|
||||||
Statistics,
|
Statistics,
|
||||||
} from "../../../../data/history";
|
} from "../../../../data/history";
|
||||||
@ -52,11 +53,9 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
if (!this._fetching && !this._stats) {
|
if (!this._fetching && !this._stats) {
|
||||||
this._fetching = true;
|
this._fetching = true;
|
||||||
Promise.all([this._getStatistics(), this._fetchCO2SignalEntity()]).then(
|
this._getStatistics().then(() => {
|
||||||
() => {
|
this._fetching = false;
|
||||||
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 =
|
const totalConsumption =
|
||||||
totalGridConsumption +
|
totalGridConsumption +
|
||||||
(totalSolarProduction || 0) -
|
(totalSolarProduction || 0) -
|
||||||
@ -133,22 +118,33 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
|||||||
|
|
||||||
let homeLowCarbonCircumference: number | undefined;
|
let homeLowCarbonCircumference: number | undefined;
|
||||||
let homeHighCarbonCircumference: number | undefined;
|
let homeHighCarbonCircumference: number | undefined;
|
||||||
if (co2percentage !== undefined) {
|
|
||||||
const gridPctHighCarbon = co2percentage / 100;
|
|
||||||
|
|
||||||
lowCarbonConsumption =
|
if (this._co2SignalEntity && this._co2SignalEntity in this._stats) {
|
||||||
totalGridConsumption - totalGridConsumption * gridPctHighCarbon;
|
// 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 =
|
if (highCarbonConsumption !== null) {
|
||||||
(gridPctHighCarbon * totalGridConsumption) / totalConsumption;
|
const gridPctHighCarbon = highCarbonConsumption / totalConsumption;
|
||||||
|
|
||||||
homeHighCarbonCircumference =
|
lowCarbonConsumption =
|
||||||
CIRCLE_CIRCUMFERENCE * homePctGridHighCarbon;
|
totalGridConsumption - totalGridConsumption * gridPctHighCarbon;
|
||||||
|
|
||||||
homeLowCarbonCircumference =
|
const homePctGridHighCarbon =
|
||||||
CIRCLE_CIRCUMFERENCE -
|
(gridPctHighCarbon * totalGridConsumption) / totalConsumption;
|
||||||
(homeSolarCircumference || 0) -
|
|
||||||
homeHighCarbonCircumference;
|
homeHighCarbonCircumference =
|
||||||
|
CIRCLE_CIRCUMFERENCE * homePctGridHighCarbon;
|
||||||
|
|
||||||
|
homeLowCarbonCircumference =
|
||||||
|
CIRCLE_CIRCUMFERENCE -
|
||||||
|
(homeSolarCircumference || 0) -
|
||||||
|
homeHighCarbonCircumference;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
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([
|
const [configEntries, entityRegistryEntries] = await Promise.all([
|
||||||
getConfigEntries(this.hass),
|
getConfigEntries(this.hass),
|
||||||
subscribeOne(this.hass.connection, subscribeEntityRegistry),
|
subscribeOne(this.hass.connection, subscribeEntityRegistry),
|
||||||
@ -372,32 +368,35 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard {
|
|||||||
(entry) => entry.domain === "co2signal"
|
(entry) => entry.domain === "co2signal"
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!co2ConfigEntry) {
|
this._co2SignalEntity = undefined;
|
||||||
return;
|
|
||||||
|
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();
|
const startDate = new Date();
|
||||||
startDate.setHours(0, 0, 0, 0);
|
startDate.setHours(0, 0, 0, 0);
|
||||||
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint
|
||||||
|
|
||||||
const statistics: string[] = [];
|
const statistics: string[] = [];
|
||||||
|
|
||||||
|
if (this._co2SignalEntity !== undefined) {
|
||||||
|
statistics.push(this._co2SignalEntity);
|
||||||
|
}
|
||||||
|
|
||||||
const prefs = this._config!.prefs;
|
const prefs = this._config!.prefs;
|
||||||
for (const source of prefs.energy_sources) {
|
for (const source of prefs.energy_sources) {
|
||||||
if (source.type === "solar") {
|
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