1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-17 07:34:21 +01:00
Files
frontend/src/data/energy.ts
Petar Petrov d9c39640e0 Preserve entity unit in gas and water flow rate badges (#30116)
* Preserve entity unit_of_measurement in gas and water flow rate badges

The gas and water total badges on the energy dashboard Now tab previously
converted all flow rate values to L/min and then formatted them as either
L/min or gal/min based on the unit system. This meant entities reporting
in m³/h or other units always displayed incorrectly.

Now the badges preserve the unit_of_measurement from the entities. If all
entities share the same unit, the raw values are summed directly. If they
differ, values are converted through L/min as an intermediate and displayed
in the first entity's unit.

* Extract shared computeTotalFlowRate to energy.ts
2026-03-18 13:00:17 +01:00

1959 lines
52 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
addDays,
addHours,
addMilliseconds,
addMonths,
differenceInDays,
differenceInMonths,
endOfDay,
startOfDay,
isFirstDayOfMonth,
isLastDayOfMonth,
addYears,
} from "date-fns";
import type { Collection, HassEntity } from "home-assistant-js-websocket";
import { getCollection } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { normalizeValueBySIPrefix } from "../common/number/normalize-by-si-prefix";
import {
calcDate,
calcDateProperty,
calcDateDifferenceProperty,
} from "../common/datetime/calc_date";
import { formatTime24h } from "../common/datetime/format_time";
import { groupBy } from "../common/util/group-by";
import { fileDownload } from "../util/file_download";
import type { HomeAssistant } from "../types";
import type {
Statistics,
StatisticsMetaData,
StatisticsUnitConfiguration,
StatisticValue,
} from "./recorder";
import {
fetchStatistics,
getDisplayUnit,
getStatisticMetadata,
VOLUME_UNITS,
} from "./recorder";
import { calcDateRange } from "../common/datetime/calc_date_range";
import type { DateRange } from "../common/datetime/calc_date_range";
import { formatNumber } from "../common/number/format_number";
export const ENERGY_COLLECTION_KEY_PREFIX = "energy_";
// All collection keys created this session
const energyCollectionKeys = new Set<string | undefined>();
// Validate that a string is a valid energy collection key.
export function validateEnergyCollectionKey(key: string | undefined) {
if (!key?.startsWith(ENERGY_COLLECTION_KEY_PREFIX)) {
throw new Error(
`Collection keys must start with ${ENERGY_COLLECTION_KEY_PREFIX}.`
);
}
}
export const emptyGridSourceEnergyPreference =
(): GridSourceTypeEnergyPreference => ({
type: "grid",
stat_energy_from: null,
stat_energy_to: null,
stat_cost: null,
stat_compensation: null,
entity_energy_price: null,
number_energy_price: null,
entity_energy_price_export: null,
number_energy_price_export: null,
cost_adjustment_day: 0,
});
export const emptySolarEnergyPreference =
(): SolarSourceTypeEnergyPreference => ({
type: "solar",
stat_energy_from: "",
config_entry_solar_forecast: null,
});
export const emptyBatteryEnergyPreference =
(): BatterySourceTypeEnergyPreference => ({
type: "battery",
stat_energy_from: "",
stat_energy_to: "",
});
export const emptyGasEnergyPreference = (): GasSourceTypeEnergyPreference => ({
type: "gas",
stat_energy_from: "",
stat_cost: null,
entity_energy_price: null,
number_energy_price: null,
});
export const emptyWaterEnergyPreference =
(): WaterSourceTypeEnergyPreference => ({
type: "water",
stat_energy_from: "",
stat_cost: null,
entity_energy_price: null,
number_energy_price: null,
});
interface EnergySolarForecast {
wh_hours: Record<string, number>;
}
export type EnergySolarForecasts = Record<string, EnergySolarForecast>;
export interface DeviceConsumptionEnergyPreference {
// This is an ever increasing value
stat_consumption: string;
stat_rate?: string;
name?: string;
included_in_stat?: string;
}
export interface PowerConfig {
stat_rate?: string; // Standard single sensor
stat_rate_inverted?: string; // Inverted single sensor
stat_rate_from?: string; // Battery: discharge / Grid: consumption
stat_rate_to?: string; // Battery: charge / Grid: return
}
/**
* Grid source format.
* Each grid connection is a single object with import/export/power together.
* Multiple grid sources are allowed.
*/
export interface GridSourceTypeEnergyPreference {
type: "grid";
// Import meter
stat_energy_from: string | null;
// Export meter
stat_energy_to: string | null;
// Import cost tracking
stat_cost: string | null;
entity_energy_price: string | null;
number_energy_price: number | null;
// Export compensation tracking
stat_compensation: string | null;
entity_energy_price_export: string | null;
number_energy_price_export: number | null;
// Power measurement
stat_rate?: string; // always available if power_config is set
power_config?: PowerConfig;
cost_adjustment_day: number;
}
export interface SolarSourceTypeEnergyPreference {
type: "solar";
stat_energy_from: string;
stat_rate?: string;
config_entry_solar_forecast: string[] | null;
}
export interface BatterySourceTypeEnergyPreference {
type: "battery";
stat_energy_from: string;
stat_energy_to: string;
stat_rate?: string; // always available if power_config is set
power_config?: PowerConfig;
}
export interface GasSourceTypeEnergyPreference {
type: "gas";
// kWh/volume meter
stat_energy_from: string;
// Flow rate (m³/h, L/min, etc.)
stat_rate?: string;
// $ meter
stat_cost: string | null;
// Can be used to generate costs if stat_cost omitted
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
}
export interface WaterSourceTypeEnergyPreference {
type: "water";
// volume meter
stat_energy_from: string;
// Flow rate (L/min, gal/min, m³/h, etc.)
stat_rate?: string;
// $ meter
stat_cost: string | null;
// Can be used to generate costs if stat_cost omitted
entity_energy_price: string | null;
number_energy_price: number | null;
unit_of_measurement?: string | null;
}
export type EnergySource =
| SolarSourceTypeEnergyPreference
| GridSourceTypeEnergyPreference
| BatterySourceTypeEnergyPreference
| GasSourceTypeEnergyPreference
| WaterSourceTypeEnergyPreference;
export interface EnergyPreferences {
energy_sources: EnergySource[];
device_consumption: DeviceConsumptionEnergyPreference[];
device_consumption_water: DeviceConsumptionEnergyPreference[];
}
export interface EnergyInfo {
cost_sensors: Record<string, string>;
solar_forecast_domains: string[];
}
export interface EnergyValidationIssue {
type: string;
affected_entities: [string, unknown][];
translation_placeholders: Record<string, string>;
}
export interface EnergyPreferencesValidation {
energy_sources: EnergyValidationIssue[][];
device_consumption: EnergyValidationIssue[][];
device_consumption_water: EnergyValidationIssue[][];
}
export const getEnergyInfo = (hass: HomeAssistant) =>
hass.callWS<EnergyInfo>({
type: "energy/info",
});
export const getEnergyPreferenceValidation = async (hass: HomeAssistant) => {
await hass.loadBackendTranslation("issues", "energy");
return hass.callWS<EnergyPreferencesValidation>({
type: "energy/validate",
});
};
export const getEnergyPreferences = (hass: HomeAssistant) =>
hass.callWS<EnergyPreferences>({
type: "energy/get_prefs",
});
export const saveEnergyPreferences = async (
hass: HomeAssistant,
prefs: Partial<EnergyPreferences>
) => {
const newPrefs = hass.callWS<EnergyPreferences>({
type: "energy/save_prefs",
...prefs,
});
clearEnergyCollectionPreferences(hass);
return newPrefs;
};
export type FossilEnergyConsumption = Record<string, number>;
export const getFossilEnergyConsumption = async (
hass: HomeAssistant,
startTime: Date,
energy_statistic_ids: string[],
co2_statistic_id: string,
endTime?: Date,
period: "5minute" | "hour" | "day" | "month" = "hour"
) =>
hass.callWS<FossilEnergyConsumption>({
type: "energy/fossil_energy_consumption",
start_time: startTime.toISOString(),
end_time: endTime?.toISOString(),
energy_statistic_ids,
co2_statistic_id,
period,
});
export interface EnergySourceByType {
grid?: GridSourceTypeEnergyPreference[];
solar?: SolarSourceTypeEnergyPreference[];
battery?: BatterySourceTypeEnergyPreference[];
gas?: GasSourceTypeEnergyPreference[];
water?: WaterSourceTypeEnergyPreference[];
}
export const energySourcesByType = (prefs: EnergyPreferences) =>
groupBy(prefs.energy_sources, (item) => item.type) as EnergySourceByType;
export interface EnergyData {
start: Date;
end?: Date;
startCompare?: Date;
endCompare?: Date;
compareMode?: CompareMode;
prefs: EnergyPreferences;
info: EnergyInfo;
stats: Statistics;
statsMetadata: Record<string, StatisticsMetaData>;
statsCompare: Statistics;
co2SignalEntity?: string;
fossilEnergyConsumption?: FossilEnergyConsumption;
fossilEnergyConsumptionCompare?: FossilEnergyConsumption;
waterUnit: string;
gasUnit: string;
}
export const getReferencedStatisticIds = (
prefs: EnergyPreferences,
info: EnergyInfo,
includeTypes?: string[]
): string[] => {
const statIDs: string[] = [];
for (const source of prefs.energy_sources) {
if (includeTypes && !includeTypes.includes(source.type)) {
continue;
}
if (source.type === "solar") {
statIDs.push(source.stat_energy_from);
continue;
}
if (source.type === "gas" || source.type === "water") {
statIDs.push(source.stat_energy_from);
if (source.stat_cost) {
statIDs.push(source.stat_cost);
}
const costStatId = info.cost_sensors[source.stat_energy_from];
if (costStatId) {
statIDs.push(costStatId);
}
continue;
}
if (source.type === "battery") {
statIDs.push(source.stat_energy_from);
statIDs.push(source.stat_energy_to);
continue;
}
// grid source
if (source.stat_energy_from) {
statIDs.push(source.stat_energy_from);
if (source.stat_cost) {
statIDs.push(source.stat_cost);
}
const importCostStatId = info.cost_sensors[source.stat_energy_from];
if (importCostStatId) {
statIDs.push(importCostStatId);
}
}
if (source.stat_energy_to) {
statIDs.push(source.stat_energy_to);
if (source.stat_compensation) {
statIDs.push(source.stat_compensation);
}
const exportCostStatId = info.cost_sensors[source.stat_energy_to];
if (exportCostStatId) {
statIDs.push(exportCostStatId);
}
}
}
if (!(includeTypes && !includeTypes.includes("device"))) {
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
}
if (!(includeTypes && !includeTypes.includes("water"))) {
statIDs.push(
...prefs.device_consumption_water.map((d) => d.stat_consumption)
);
}
return statIDs;
};
export const getReferencedStatisticIdsPower = (
prefs: EnergyPreferences
): string[] => {
const statIDs: (string | undefined)[] = [];
for (const source of prefs.energy_sources) {
if (source.type === "gas" || source.type === "water") {
if (source.stat_rate) {
statIDs.push(source.stat_rate);
}
continue;
}
if (source.type === "solar") {
statIDs.push(source.stat_rate);
continue;
}
if (source.type === "battery") {
if (source.stat_rate) {
statIDs.push(source.stat_rate);
}
continue;
}
// grid source
if (source.stat_rate) {
statIDs.push(source.stat_rate);
}
}
statIDs.push(...prefs.device_consumption.map((d) => d.stat_rate));
statIDs.push(...prefs.device_consumption_water.map((d) => d.stat_rate));
return statIDs.filter(Boolean) as string[];
};
export const enum CompareMode {
NONE = "",
PREVIOUS = "previous",
YOY = "yoy",
}
const getEnergyData = async (
hass: HomeAssistant,
prefs: EnergyPreferences,
start: Date,
end?: Date,
compare?: CompareMode
): Promise<EnergyData> => {
const info = await getEnergyInfo(hass);
let co2SignalEntity: string | undefined;
for (const entity of Object.values(hass.entities)) {
if (entity.platform !== "co2signal") {
continue;
}
// The integration offers 2 entities. We want the % one.
const co2State = hass.states[entity.entity_id];
if (!co2State || co2State.attributes.unit_of_measurement !== "%") {
continue;
}
co2SignalEntity = co2State.entity_id;
break;
}
const consumptionStatIDs: string[] = [];
for (const source of prefs.energy_sources) {
if (source.type === "grid" && source.stat_energy_from) {
consumptionStatIDs.push(source.stat_energy_from);
}
}
const energyStatIds = getReferencedStatisticIds(prefs, info, [
"grid",
"solar",
"battery",
"gas",
"device",
]);
const powerStatIds = getReferencedStatisticIdsPower(prefs);
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
const dayDifference = differenceInDays(end || new Date(), start);
const period = getSuggestedPeriod(start, end);
const finePeriod = getSuggestedPeriod(start, end, true);
const statsMetadata: Record<string, StatisticsMetaData> = {};
const statsMetadataArray = allStatIDs.length
? await getStatisticMetadata(hass, allStatIDs)
: [];
if (allStatIDs.length) {
statsMetadataArray.forEach((x) => {
statsMetadata[x.statistic_id] = x;
});
}
const gasUnit = getEnergyGasUnit(hass, prefs, statsMetadata);
const gasIsVolume = VOLUME_UNITS.includes(gasUnit as any);
const energyUnits: StatisticsUnitConfiguration = {
energy: "kWh",
volume: gasIsVolume
? (gasUnit as (typeof VOLUME_UNITS)[number])
: undefined,
};
const powerUnits: StatisticsUnitConfiguration = {
power: "kW",
};
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
const waterUnits: StatisticsUnitConfiguration = {
volume: waterUnit,
};
const _energyStats: Statistics | Promise<Statistics> = energyStatIds.length
? fetchStatistics(hass!, start, end, energyStatIds, period, energyUnits, [
"change",
])
: {};
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
"mean",
])
: {};
// If power stats 5 minute data is selected, then also fetch hourly data which
// will be used to back-fill any missing data points in the 5 minute data when
// the requested range is beyond the limit of short term statistics.
const _powerStatsHour: Statistics | Promise<Statistics> =
powerStatIds.length && finePeriod === "5minute"
? fetchStatistics(hass!, start, end, powerStatIds, "hour", powerUnits, [
"mean",
])
: {};
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
"change",
])
: {};
let statsCompare;
let startCompare;
let endCompare;
let _energyStatsCompare: Statistics | Promise<Statistics> = {};
let _waterStatsCompare: Statistics | Promise<Statistics> = {};
if (compare) {
if (compare === CompareMode.PREVIOUS) {
if (
(calcDateProperty(
start,
isFirstDayOfMonth,
hass.locale,
hass.config
) as boolean) &&
(calcDateProperty(
end || new Date(),
isLastDayOfMonth,
hass.locale,
hass.config
) as boolean)
) {
// When comparing a month (or multiple), we want to start at the beginning of the month
startCompare = calcDate(
start,
addMonths,
hass.locale,
hass.config,
-(calcDateDifferenceProperty(
end || new Date(),
start,
differenceInMonths,
hass.locale,
hass.config
) as number) - 1
);
} else {
startCompare = calcDate(
start,
addDays,
hass.locale,
hass.config,
(dayDifference + 1) * -1
);
}
endCompare = addMilliseconds(start, -1);
} else if (compare === CompareMode.YOY) {
startCompare = calcDate(start, addYears, hass.locale, hass.config, -1);
endCompare = calcDate(end!, addYears, hass.locale, hass.config, -1);
}
if (energyStatIds.length) {
_energyStatsCompare = fetchStatistics(
hass!,
startCompare,
endCompare,
energyStatIds,
period,
energyUnits,
["change"]
);
}
if (waterStatIds.length) {
_waterStatsCompare = fetchStatistics(
hass!,
startCompare,
endCompare,
waterStatIds,
period,
waterUnits,
["change"]
);
}
}
let _fossilEnergyConsumption: undefined | Promise<FossilEnergyConsumption>;
let _fossilEnergyConsumptionCompare:
| undefined
| Promise<FossilEnergyConsumption>;
if (co2SignalEntity !== undefined) {
_fossilEnergyConsumption = getFossilEnergyConsumption(
hass!,
start,
consumptionStatIDs,
co2SignalEntity,
end,
period
);
if (compare) {
_fossilEnergyConsumptionCompare = getFossilEnergyConsumption(
hass!,
startCompare,
consumptionStatIDs,
co2SignalEntity,
endCompare,
period
);
}
}
const [
energyStats,
powerStats,
powerStatsHour,
waterStats,
energyStatsCompare,
waterStatsCompare,
fossilEnergyConsumption,
fossilEnergyConsumptionCompare,
] = await Promise.all([
_energyStats,
_powerStats,
_powerStatsHour,
_waterStats,
_energyStatsCompare,
_waterStatsCompare,
_fossilEnergyConsumption,
_fossilEnergyConsumptionCompare,
]);
// Back-fill any missing power statistics from hourly data if present
if (Object.keys(powerStatsHour).length) {
powerStatIds.forEach((powerId) => {
if (powerId in powerStatsHour) {
// If we have extra hourly power statistics for an ID, we may need to
// insert data into statistics
if (powerId in powerStats && powerStats[powerId].length) {
// We have 5-minute data. Only insert hourly values for time periods
// before the first 5-minute value.
const powerStatFirst = powerStats[powerId][0];
const powerStatHour = powerStatsHour[powerId];
let powerStatHourLast = 0;
for (const powerStat of powerStatHour) {
if (powerStat.end > powerStatFirst.start) {
break;
}
powerStatHourLast++;
}
powerStats[powerId] = [
...powerStatHour.slice(0, powerStatHourLast),
...powerStats[powerId],
];
} else {
// There was no 5-minute data, so simply insert full hourly data
powerStats[powerId] = powerStatsHour[powerId];
}
}
});
}
const stats = { ...energyStats, ...waterStats, ...powerStats };
if (compare) {
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
}
const data: EnergyData = {
start,
end,
startCompare,
endCompare,
compareMode: compare,
info,
prefs,
stats,
statsMetadata,
statsCompare,
co2SignalEntity,
fossilEnergyConsumption,
fossilEnergyConsumptionCompare,
waterUnit,
gasUnit,
};
return data;
};
export interface EnergyCollection extends Collection<EnergyData> {
start: Date;
end?: Date;
compare?: CompareMode;
prefs?: EnergyPreferences;
clearPrefs(): void;
setPeriod(newStart: Date, newEnd?: Date): void;
setCompare(compare: CompareMode): void;
isActive(): boolean;
_refreshTimeout?: number;
_updatePeriodTimeout?: number;
_active: number;
}
const clearEnergyCollectionPreferences = (hass: HomeAssistant) => {
energyCollectionKeys.forEach((key) => {
const energyCollection = findEnergyDataCollection(hass, key);
if (energyCollection) {
energyCollection.clearPrefs();
if (energyCollection.isActive()) {
energyCollection.refresh();
}
}
});
};
const scheduleHourlyRefresh = (collection: EnergyCollection) => {
if (collection._refreshTimeout) {
clearTimeout(collection._refreshTimeout);
}
if (collection._active && (!collection.end || collection.end > new Date())) {
// The stats are created every hour
// Schedule a refresh for 20 minutes past the hour
// If the end is larger than the current time.
const nextFetch = new Date();
if (nextFetch.getMinutes() >= 20) {
nextFetch.setHours(nextFetch.getHours() + 1);
}
nextFetch.setMinutes(20, 0, 0);
collection._refreshTimeout = window.setTimeout(
() => collection.refresh(),
nextFetch.getTime() - Date.now()
);
}
};
const convertCollectionKeyToConnection = (
hass: HomeAssistant,
collectionKey: string | undefined
): [string, string | undefined] => {
let key = "_energy";
if (collectionKey) {
validateEnergyCollectionKey(collectionKey);
key = `_${collectionKey}`;
} else if (hass.panelUrl) {
const defaultKey = ENERGY_COLLECTION_KEY_PREFIX + hass.panelUrl;
key = `_${defaultKey}`;
collectionKey = defaultKey;
}
return [key, collectionKey];
};
const findEnergyDataCollection = (
hass: HomeAssistant,
collectionKey: string | undefined
): EnergyCollection | undefined => {
// Lookup the connection key and default key name
const [key, _collectionKey] = convertCollectionKeyToConnection(
hass,
collectionKey
);
return (hass.connection as any)[key];
};
export const getEnergyDataCollection = (
hass: HomeAssistant,
options: { prefs?: EnergyPreferences; key?: string } = {}
): EnergyCollection => {
const [key, collectionKey] = convertCollectionKeyToConnection(
hass,
options.key
);
if ((hass.connection as any)[key]) {
return (hass.connection as any)[key];
}
energyCollectionKeys.add(collectionKey);
const collection = getCollection<EnergyData>(
hass.connection,
key,
async () => {
if (!collection.prefs) {
// This will raise if not found.
// Detect by checking `e.code === "not_found"
collection.prefs = await getEnergyPreferences(hass);
}
scheduleHourlyRefresh(collection);
return getEnergyData(
hass,
collection.prefs,
collection.start,
collection.end,
collection.compare
);
}
) as EnergyCollection;
const origSubscribe = collection.subscribe;
collection.subscribe = (subscriber: (data: EnergyData) => void) => {
const unsub = origSubscribe(subscriber);
collection._active++;
if (collection._refreshTimeout === undefined) {
scheduleHourlyRefresh(collection);
}
return () => {
collection._active--;
if (collection._active < 1) {
clearTimeout(collection._refreshTimeout);
collection._refreshTimeout = undefined;
}
unsub();
};
};
collection._active = 0;
collection.prefs = options.prefs;
const now = new Date();
const hour = formatTime24h(now, hass.locale, hass.config).split(":")[0];
// Set start to start of today if we have data for today, otherwise yesterday
const preferredPeriod =
(localStorage.getItem(`energy-default-period-${key}`) as DateRange) ||
"today";
const period =
preferredPeriod === "today" && hour === "0" ? "yesterday" : preferredPeriod;
const [start, end] = calcDateRange(hass, period);
collection.start = calcDate(start, startOfDay, hass.locale, hass.config);
collection.end = calcDate(end, endOfDay, hass.locale, hass.config);
const scheduleUpdatePeriod = () => {
collection._updatePeriodTimeout = window.setTimeout(
() => {
collection.start = calcDate(
new Date(),
startOfDay,
hass.locale,
hass.config
);
collection.end = calcDate(
new Date(),
endOfDay,
hass.locale,
hass.config
);
collection.refresh();
scheduleUpdatePeriod();
},
addHours(
calcDate(new Date(), endOfDay, hass.locale, hass.config),
1
).getTime() - Date.now() // Switch to next day an hour after the day changed
);
};
scheduleUpdatePeriod();
collection.isActive = () => !!collection._active;
collection.clearPrefs = () => {
collection.prefs = undefined;
};
collection.setPeriod = (newStart: Date, newEnd?: Date) => {
if (collection._updatePeriodTimeout) {
clearTimeout(collection._updatePeriodTimeout);
collection._updatePeriodTimeout = undefined;
}
collection.start = newStart;
collection.end = newEnd;
if (
collection.start.getTime() ===
calcDate(new Date(), startOfDay, hass.locale, hass.config).getTime() &&
collection.end?.getTime() ===
calcDate(new Date(), endOfDay, hass.locale, hass.config).getTime()
) {
scheduleUpdatePeriod();
}
};
collection.setCompare = (compare: CompareMode) => {
collection.compare = compare;
};
return collection;
};
export const getEnergySolarForecasts = (hass: HomeAssistant) =>
hass.callWS<EnergySolarForecasts>({
type: "energy/solar_forecast",
});
const energyGasUnitClass = ["volume", "energy"] as const;
export type EnergyGasUnitClass = (typeof energyGasUnitClass)[number];
export const getEnergyGasUnitClass = (
prefs: EnergyPreferences,
excludeSource?: string,
statisticsMetaData: Record<string, StatisticsMetaData> = {}
): EnergyGasUnitClass | undefined => {
for (const source of prefs.energy_sources) {
if (source.type !== "gas") {
continue;
}
if (excludeSource && excludeSource === source.stat_energy_from) {
continue;
}
const statisticIdWithMeta = statisticsMetaData[source.stat_energy_from];
if (
energyGasUnitClass.includes(
statisticIdWithMeta?.unit_class as EnergyGasUnitClass
)
) {
return statisticIdWithMeta.unit_class as EnergyGasUnitClass;
}
}
return undefined;
};
const getEnergyGasUnit = (
hass: HomeAssistant,
prefs: EnergyPreferences,
statisticsMetaData: Record<string, StatisticsMetaData> = {}
): string => {
const unitClass = getEnergyGasUnitClass(prefs, undefined, statisticsMetaData);
if (unitClass === "energy") {
return "kWh";
}
const units = prefs.energy_sources
.filter((s) => s.type === "gas")
.map((s) =>
getDisplayUnit(
hass,
s.stat_energy_from,
statisticsMetaData[s.stat_energy_from]
)
);
if (units.length) {
const first = units[0];
if (
VOLUME_UNITS.includes(first as any) &&
units.every((u) => u === first)
) {
return first as (typeof VOLUME_UNITS)[number];
}
}
return hass.config.unit_system.length === "km" ? "m³" : "ft³";
};
const getEnergyWaterUnit = (
hass: HomeAssistant,
prefs: EnergyPreferences,
statisticsMetaData: Record<string, StatisticsMetaData>
): (typeof VOLUME_UNITS)[number] => {
const units = prefs.energy_sources
.filter((s) => s.type === "water")
.map((s) =>
getDisplayUnit(
hass,
s.stat_energy_from,
statisticsMetaData[s.stat_energy_from]
)
);
if (units.length) {
const first = units[0];
if (
VOLUME_UNITS.includes(first as any) &&
units.every((u) => u === first)
) {
return first as (typeof VOLUME_UNITS)[number];
}
}
return hass.config.unit_system.length === "km" ? "L" : "gal";
};
export const energyStatisticHelpUrl =
"/docs/energy/faq/#troubleshooting-missing-entities";
export interface EnergySumData {
to_grid?: Record<number, number>;
from_grid?: Record<number, number>;
to_battery?: Record<number, number>;
from_battery?: Record<number, number>;
solar?: Record<number, number>;
total: {
to_grid?: number;
from_grid?: number;
to_battery?: number;
from_battery?: number;
solar?: number;
};
timestamps: number[];
}
export interface EnergyConsumptionData {
used_total: Record<number, number>;
grid_to_battery: Record<number, number>;
battery_to_grid: Record<number, number>;
solar_to_battery: Record<number, number>;
solar_to_grid: Record<number, number>;
used_solar: Record<number, number>;
used_grid: Record<number, number>;
used_battery: Record<number, number>;
total: {
used_total: number;
grid_to_battery: number;
battery_to_grid: number;
solar_to_battery: number;
solar_to_grid: number;
used_solar: number;
used_grid: number;
used_battery: number;
};
}
export const getSummedData = memoizeOne(
(
data: EnergyData
): { summedData: EnergySumData; compareSummedData?: EnergySumData } => {
const summedData = getSummedDataPartial(data);
const compareSummedData = data.statsCompare
? getSummedDataPartial(data, true)
: undefined;
return { summedData, compareSummedData };
}
);
const getSummedDataPartial = (
data: EnergyData,
compare?: boolean
): EnergySumData => {
const statIds: {
to_grid?: string[];
from_grid?: string[];
solar?: string[];
to_battery?: string[];
from_battery?: string[];
} = {};
for (const source of data.prefs.energy_sources) {
if (source.type === "solar") {
if (statIds.solar) {
statIds.solar.push(source.stat_energy_from);
} else {
statIds.solar = [source.stat_energy_from];
}
continue;
}
if (source.type === "battery") {
if (statIds.to_battery) {
statIds.to_battery.push(source.stat_energy_to);
statIds.from_battery!.push(source.stat_energy_from);
} else {
statIds.to_battery = [source.stat_energy_to];
statIds.from_battery = [source.stat_energy_from];
}
continue;
}
if (source.type !== "grid") {
continue;
}
// grid source
if (source.stat_energy_from) {
if (statIds.from_grid) {
statIds.from_grid.push(source.stat_energy_from);
} else {
statIds.from_grid = [source.stat_energy_from];
}
}
if (source.stat_energy_to) {
if (statIds.to_grid) {
statIds.to_grid.push(source.stat_energy_to);
} else {
statIds.to_grid = [source.stat_energy_to];
}
}
}
const summedData: EnergySumData = { total: {}, timestamps: [] };
const timestamps = new Set<number>();
Object.entries(statIds).forEach(([key, subStatIds]) => {
const totalStats: Record<number, number> = {};
const sets: Record<string, Record<number, number>> = {};
let sum = 0;
subStatIds!.forEach((id) => {
const stats = compare ? data.statsCompare[id] : data.stats[id];
if (!stats) {
return;
}
const set = {};
stats.forEach((stat) => {
if (stat.change === null || stat.change === undefined) {
return;
}
const val = stat.change;
sum += val;
totalStats[stat.start] =
stat.start in totalStats ? totalStats[stat.start] + val : val;
timestamps.add(stat.start);
});
sets[id] = set;
});
summedData[key] = totalStats;
summedData.total[key] = sum;
});
summedData.timestamps = Array.from(timestamps).sort();
return summedData;
};
export const computeConsumptionData = memoizeOne(
(
data: EnergySumData,
compareData?: EnergySumData
): {
consumption: EnergyConsumptionData;
compareConsumption?: EnergyConsumptionData;
} => {
const consumption = computeConsumptionDataPartial(data);
const compareConsumption = compareData
? computeConsumptionDataPartial(compareData)
: undefined;
return { consumption, compareConsumption };
}
);
const computeConsumptionDataPartial = (
data: EnergySumData
): EnergyConsumptionData => {
const outData: EnergyConsumptionData = {
used_total: {},
grid_to_battery: {},
battery_to_grid: {},
solar_to_battery: {},
solar_to_grid: {},
used_solar: {},
used_grid: {},
used_battery: {},
total: {
used_total: 0,
grid_to_battery: 0,
battery_to_grid: 0,
solar_to_battery: 0,
solar_to_grid: 0,
used_solar: 0,
used_grid: 0,
used_battery: 0,
},
};
data.timestamps.forEach((t) => {
const {
grid_to_battery,
battery_to_grid,
used_solar,
used_grid,
used_battery,
used_total,
solar_to_battery,
solar_to_grid,
} = computeConsumptionSingle({
from_grid: data.from_grid && (data.from_grid[t] ?? 0),
to_grid: data.to_grid && (data.to_grid[t] ?? 0),
solar: data.solar && (data.solar[t] ?? 0),
to_battery: data.to_battery && (data.to_battery[t] ?? 0),
from_battery: data.from_battery && (data.from_battery[t] ?? 0),
});
outData.used_total[t] = used_total;
outData.total.used_total += used_total;
outData.grid_to_battery[t] = grid_to_battery;
outData.total.grid_to_battery += grid_to_battery;
outData.battery_to_grid![t] = battery_to_grid;
outData.total.battery_to_grid += battery_to_grid;
outData.used_battery![t] = used_battery;
outData.total.used_battery += used_battery;
outData.used_grid![t] = used_grid;
outData.total.used_grid += used_grid;
outData.used_solar![t] = used_solar;
outData.total.used_solar += used_solar;
outData.solar_to_battery[t] = solar_to_battery;
outData.total.solar_to_battery += solar_to_battery;
outData.solar_to_grid[t] = solar_to_grid;
outData.total.solar_to_grid += solar_to_grid;
});
return outData;
};
export const computeConsumptionSingle = (data: {
from_grid: number | undefined;
to_grid: number | undefined;
solar: number | undefined;
to_battery: number | undefined;
from_battery: number | undefined;
}): {
grid_to_battery: number;
battery_to_grid: number;
solar_to_battery: number;
solar_to_grid: number;
used_solar: number;
used_grid: number;
used_battery: number;
used_total: number;
} => {
let to_grid = Math.max(data.to_grid || 0, 0);
let to_battery = Math.max(data.to_battery || 0, 0);
let solar = Math.max(data.solar || 0, 0);
let from_grid = Math.max(data.from_grid || 0, 0);
let from_battery = Math.max(data.from_battery || 0, 0);
const used_total =
(from_grid || 0) +
(solar || 0) +
(from_battery || 0) -
(to_grid || 0) -
(to_battery || 0);
let used_solar = 0;
let grid_to_battery = 0;
let battery_to_grid = 0;
let solar_to_battery = 0;
let solar_to_grid = 0;
let used_battery = 0;
let used_grid = 0;
let used_total_remaining = Math.max(used_total, 0);
// Consumption Priority
// Solar -> Battery_In
// Solar -> Grid_Out
// Battery_Out -> Grid_Out
// Grid_In -> Battery_In
// Solar -> Consumption
// Battery_Out -> Consumption
// Grid_In -> Consumption
// If we have more grid_in than consumption, the excess must be charging the battery
// This must be accounted for before filling the battery from solar, or else the grid
// input could be stranded with nowhere to go.
const excess_grid_in_after_consumption = Math.max(
0,
Math.min(to_battery, from_grid - used_total_remaining)
);
grid_to_battery += excess_grid_in_after_consumption;
to_battery -= excess_grid_in_after_consumption;
from_grid -= excess_grid_in_after_consumption;
// Fill the remainder of the battery input from solar
// Solar -> Battery_In
solar_to_battery = Math.min(solar, to_battery);
to_battery -= solar_to_battery;
solar -= solar_to_battery;
// Solar -> Grid_Out
solar_to_grid = Math.min(solar, to_grid);
to_grid -= solar_to_grid;
solar -= solar_to_grid;
// Battery_Out -> Grid_Out
battery_to_grid = Math.min(from_battery, to_grid);
from_battery -= battery_to_grid;
to_grid -= battery_to_grid;
// Grid_In -> Battery_In (second pass)
const grid_to_battery_2 = Math.min(from_grid, to_battery);
grid_to_battery += grid_to_battery_2;
from_grid -= grid_to_battery_2;
to_battery -= grid_to_battery_2;
// Solar -> Consumption
used_solar = Math.min(used_total_remaining, solar);
used_total_remaining -= used_solar;
solar -= used_solar;
// Battery_Out -> Consumption
used_battery = Math.min(from_battery, used_total_remaining);
from_battery -= used_battery;
used_total_remaining -= used_battery;
// Grid_In -> Consumption
used_grid = Math.min(used_total_remaining, from_grid);
from_grid -= used_grid;
used_total_remaining -= from_grid;
return {
used_solar,
used_grid,
used_battery,
used_total,
grid_to_battery,
battery_to_grid,
solar_to_battery,
solar_to_grid,
};
};
export const formatConsumptionShort = (
hass: HomeAssistant,
consumption: number | null,
unit: string,
targetUnit?: string
): string => {
const units = ["Wh", "kWh", "MWh", "GWh", "TWh"];
let pickedUnit = unit;
let val = consumption || 0;
let targetUnitIndex = -1;
if (targetUnit) {
targetUnitIndex = units.findIndex((u) => u === targetUnit);
}
let unitIndex = units.findIndex((u) => u === unit);
if (unitIndex >= 0) {
while (
targetUnitIndex > -1
? targetUnitIndex < unitIndex
: Math.abs(val) < 1 && unitIndex > 0
) {
val *= 1000;
unitIndex--;
}
while (
targetUnitIndex > -1
? targetUnitIndex > unitIndex
: Math.abs(val) >= 1000 && unitIndex < units.length - 1
) {
val /= 1000;
unitIndex++;
}
pickedUnit = units[unitIndex];
}
return (
formatNumber(val, hass.locale, {
maximumFractionDigits:
Math.abs(val) < 10 ? 2 : Math.abs(val) < 100 ? 1 : 0,
}) +
" " +
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;
};
/**
* Conversion factors from each flow rate unit to L/min.
* All HA-supported UnitOfVolumeFlowRate values are covered.
*
* m³/h → 1000/60 = 16.6667 L/min
* m³/min → 1000 L/min
* m³/s → 60000 L/min
* ft³/min→ 28.3168 L/min
* L/h → 1/60 L/min
* L/min → 1 L/min
* L/s → 60 L/min
* gal/h → 3.78541/60 L/min
* gal/min→ 3.78541 L/min
* gal/d → 3.78541/1440 L/min
* mL/s → 0.06 L/min
*/
/** Exact number of liters in one US gallon */
const LITERS_PER_GALLON = 3.785411784;
export const FLOW_RATE_TO_LMIN: Record<string, number> = {
"m³/h": 1000 / 60,
"m³/min": 1000,
"m³/s": 60000,
"ft³/min": 28.316846592,
"L/h": 1 / 60,
"L/min": 1,
"L/s": 60,
"gal/h": LITERS_PER_GALLON / 60,
"gal/min": LITERS_PER_GALLON,
"gal/d": LITERS_PER_GALLON / 1440,
"mL/s": 60 / 1000,
};
/**
* Get current flow rate from an entity state, converted to L/min.
* @returns Flow rate in L/min, or undefined if unavailable/invalid.
*/
export const getFlowRateFromState = (
stateObj?: HassEntity
): number | undefined => {
if (!stateObj) {
return undefined;
}
const value = parseFloat(stateObj.state);
if (isNaN(value)) {
return undefined;
}
const unit = stateObj.attributes.unit_of_measurement;
const factor = unit ? FLOW_RATE_TO_LMIN[unit] : undefined;
if (factor === undefined) {
// Unknown unit return raw value as-is (best effort)
return value;
}
return value * factor;
};
/**
* Compute the total flow rate across all energy sources of a given type.
* Used by gas and water total badges.
*/
export const computeTotalFlowRate = (
sourceType: "gas" | "water",
prefs: EnergyPreferences,
states: HomeAssistant["states"],
entities: Set<string>
): { value: number; unit: string } => {
entities.clear();
let targetUnit: string | undefined;
let totalFlow = 0;
prefs.energy_sources.forEach((source) => {
if (source.type !== sourceType || !source.stat_rate) {
return;
}
const entityId = source.stat_rate;
entities.add(entityId);
const stateObj = states[entityId];
if (!stateObj) {
return;
}
const rawValue = parseFloat(stateObj.state);
if (isNaN(rawValue) || rawValue <= 0) {
return;
}
const entityUnit = stateObj.attributes.unit_of_measurement;
if (!entityUnit) {
return;
}
if (targetUnit === undefined) {
targetUnit = entityUnit;
totalFlow += rawValue;
return;
}
if (entityUnit === targetUnit) {
totalFlow += rawValue;
return;
}
const sourceFactor = FLOW_RATE_TO_LMIN[entityUnit];
const targetFactor = FLOW_RATE_TO_LMIN[targetUnit];
if (sourceFactor !== undefined && targetFactor !== undefined) {
totalFlow += (rawValue * sourceFactor) / targetFactor;
} else {
totalFlow += rawValue;
}
});
return {
value: Math.max(0, totalFlow),
unit: targetUnit ?? "",
};
};
/**
* Format a flow rate value (in L/min) to a human-readable string using
* the preferred unit system: metric → L/min, imperial → gal/min.
*/
export const formatFlowRateShort = (
hassLocale: HomeAssistant["locale"],
lengthUnitSystem: string,
litersPerMin: number
): string => {
const isMetric = lengthUnitSystem === "km";
if (isMetric) {
return `${formatNumber(litersPerMin, hassLocale, { maximumFractionDigits: 1 })} L/min`;
}
const galPerMin = litersPerMin / LITERS_PER_GALLON;
return `${formatNumber(galPerMin, hassLocale, { maximumFractionDigits: 1 })} gal/min`;
};
/**
* Get current power value from entity state, normalized to watts (W)
* @param stateObj - The entity state object to get power value from
* @returns Power value in W (watts), or undefined if entity not found or invalid
*/
export const getPowerFromState = (stateObj: HassEntity): number | undefined => {
if (!stateObj) {
return undefined;
}
const value = parseFloat(stateObj.state);
if (isNaN(value)) {
return undefined;
}
return normalizeValueBySIPrefix(
value,
stateObj.attributes.unit_of_measurement
);
};
/**
* Format power value in watts (W) to a short string with the appropriate unit
* @param hass - The HomeAssistant instance
* @param powerWatts - The power value in watts (W)
* @returns A string with the formatted power value and unit
*/
export const formatPowerShort = (
hass: HomeAssistant,
powerWatts: number
): string => {
const units = ["W", "kW", "MW", "GW", "TW"];
let unitIndex = 0;
let value = powerWatts;
// Scale the unit to the appropriate power of 1000
while (Math.abs(value) >= 1000 && unitIndex < units.length - 1) {
value /= 1000;
unitIndex++;
}
return (
formatNumber(value, hass.locale, {
// For watts, show no decimals. For kW and above, always show 3 decimals.
maximumFractionDigits: units[unitIndex] === "W" ? 0 : 3,
}) +
" " +
units[unitIndex]
);
};
export function getSuggestedPeriod(
start: Date,
end?: Date,
fine = false
): "5minute" | "hour" | "day" | "month" {
const dayDifference = differenceInDays(end || new Date(), start);
if (fine) {
return dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
}
return isFirstDayOfMonth(start) &&
(!end || isLastDayOfMonth(end)) &&
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour";
}
export const downloadEnergyData = (
hass: HomeAssistant,
collectionKey?: string
) => {
const energyData = getEnergyDataCollection(hass, {
key: collectionKey,
});
if (!energyData.prefs || !energyData.state.stats) {
return;
}
const gasUnit = energyData.state.gasUnit;
const electricUnit = "kWh";
const energy_sources = energyData.prefs.energy_sources;
const device_consumption = energyData.prefs.device_consumption;
const device_consumption_water = energyData.prefs.device_consumption_water;
const stats = energyData.state.stats;
const timeSet = new Set<number>();
Object.values(stats).forEach((stat) => {
stat.forEach((datapoint) => {
timeSet.add(datapoint.start);
});
});
const times = Array.from(timeSet).sort();
const headers =
"entity_id,type,unit," +
times.map((t) => new Date(t).toISOString()).join(",") +
"\n";
const csv: string[] = [];
csv[0] = headers;
const processCsvRow = function (
id: string,
type: string,
unit: string,
data: StatisticValue[]
) {
let n = 0;
const row: string[] = [];
row.push(id);
row.push(type);
row.push(unit.normalize("NFKD"));
times.forEach((t) => {
if (n < data.length && data[n].start === t) {
row.push((data[n].change ?? "").toString());
n++;
} else {
row.push("");
}
});
csv.push(row.join(",") + "\n");
};
const processStat = function (stat: string, type: string, unit: string) {
if (!stats[stat]) {
return;
}
processCsvRow(stat, type, unit, stats[stat]);
};
const currency = hass.config.currency;
const printCategory = function (
type: string,
statIds: string[],
unit: string,
costType?: string,
costStatIds?: string[]
) {
if (statIds.length) {
statIds.forEach((stat) => processStat(stat, type, unit));
if (costType && costStatIds) {
costStatIds.forEach((stat) => processStat(stat, costType, currency));
}
}
};
const grid_consumptions: string[] = [];
const grid_productions: string[] = [];
const grid_consumptions_cost: string[] = [];
const grid_productions_cost: string[] = [];
energy_sources
.filter((s) => s.type === "grid")
.forEach((source) => {
const gridSource = source as GridSourceTypeEnergyPreference;
if (gridSource.stat_energy_from) {
grid_consumptions.push(gridSource.stat_energy_from);
const importCostId =
gridSource.stat_cost ||
energyData.state.info.cost_sensors[gridSource.stat_energy_from];
if (importCostId) {
grid_consumptions_cost.push(importCostId);
}
}
if (gridSource.stat_energy_to) {
grid_productions.push(gridSource.stat_energy_to);
const exportCostId =
gridSource.stat_compensation ||
energyData.state.info.cost_sensors[gridSource.stat_energy_to];
if (exportCostId) {
grid_productions_cost.push(exportCostId);
}
}
});
printCategory(
"grid_consumption",
grid_consumptions,
electricUnit,
"grid_consumption_cost",
grid_consumptions_cost
);
printCategory(
"grid_return",
grid_productions,
electricUnit,
"grid_return_compensation",
grid_productions_cost
);
const battery_ins: string[] = [];
const battery_outs: string[] = [];
energy_sources
.filter((s) => s.type === "battery")
.forEach((source) => {
source = source as BatterySourceTypeEnergyPreference;
battery_ins.push(source.stat_energy_to);
battery_outs.push(source.stat_energy_from);
});
printCategory("battery_in", battery_ins, electricUnit);
printCategory("battery_out", battery_outs, electricUnit);
const solar_productions: string[] = [];
energy_sources
.filter((s) => s.type === "solar")
.forEach((source) => {
source = source as SolarSourceTypeEnergyPreference;
solar_productions.push(source.stat_energy_from);
});
printCategory("solar_production", solar_productions, electricUnit);
const gas_consumptions: string[] = [];
const gas_consumptions_cost: string[] = [];
energy_sources
.filter((s) => s.type === "gas")
.forEach((source) => {
source = source as GasSourceTypeEnergyPreference;
const statId = source.stat_energy_from;
gas_consumptions.push(statId);
const costId =
source.stat_cost || energyData.state.info.cost_sensors[statId];
if (costId) {
gas_consumptions_cost.push(costId);
}
});
printCategory(
"gas_consumption",
gas_consumptions,
gasUnit,
"gas_consumption_cost",
gas_consumptions_cost
);
const water_consumptions: string[] = [];
const water_consumptions_cost: string[] = [];
energy_sources
.filter((s) => s.type === "water")
.forEach((source) => {
source = source as WaterSourceTypeEnergyPreference;
const statId = source.stat_energy_from;
water_consumptions.push(statId);
const costId =
source.stat_cost || energyData.state.info.cost_sensors[statId];
if (costId) {
water_consumptions_cost.push(costId);
}
});
printCategory(
"water_consumption",
water_consumptions,
energyData.state.waterUnit,
"water_consumption_cost",
water_consumptions_cost
);
const devices: string[] = [];
device_consumption.forEach((source) => {
source = source as DeviceConsumptionEnergyPreference;
devices.push(source.stat_consumption);
});
printCategory("device_consumption", devices, electricUnit);
if (device_consumption_water) {
const waterDevices: string[] = [];
device_consumption_water.forEach((source) => {
source = source as DeviceConsumptionEnergyPreference;
waterDevices.push(source.stat_consumption);
});
printCategory(
"device_consumption_water",
waterDevices,
energyData.state.waterUnit
);
}
const { summedData } = getSummedData(energyData.state);
const { consumption } = computeConsumptionData(summedData, undefined);
const processConsumptionData = function (
type: string,
unit: string,
data: Record<number, number>
) {
const data2: StatisticValue[] = [];
Object.entries(data).forEach(([t, value]) => {
data2.push({
start: Number(t),
end: NaN,
change: value,
});
});
processCsvRow("", type, unit, data2);
};
const hasSolar = !!solar_productions.length;
const hasBattery = !!battery_ins.length;
const hasGridReturn = !!grid_productions.length;
const hasGridSource = !!grid_consumptions.length;
if (hasGridSource) {
processConsumptionData(
"calculated_consumed_grid",
electricUnit,
consumption.used_grid
);
if (hasBattery) {
processConsumptionData(
"calculated_grid_to_battery",
electricUnit,
consumption.grid_to_battery
);
}
}
if (hasGridReturn && hasBattery) {
processConsumptionData(
"calculated_battery_to_grid",
electricUnit,
consumption.battery_to_grid
);
}
if (hasBattery) {
processConsumptionData(
"calculated_consumed_battery",
electricUnit,
consumption.used_battery
);
}
if (hasSolar) {
processConsumptionData(
"calculated_consumed_solar",
electricUnit,
consumption.used_solar
);
if (hasBattery) {
processConsumptionData(
"calculated_solar_to_battery",
electricUnit,
consumption.solar_to_battery
);
}
if (hasGridReturn) {
processConsumptionData(
"calculated_solar_to_grid",
electricUnit,
consumption.solar_to_grid
);
}
}
if ((hasGridSource ? 1 : 0) + (hasSolar ? 1 : 0) + (hasBattery ? 1 : 0) > 1) {
processConsumptionData(
"calculated_total_consumption",
electricUnit,
consumption.used_total
);
}
const blob = new Blob(csv, {
type: "text/csv",
});
const url = window.URL.createObjectURL(blob);
fileDownload(url, "energy.csv");
};