From a479c6e7860dd1f933904773de23ecd9f2609884 Mon Sep 17 00:00:00 2001
From: karwosts <32912880+karwosts@users.noreply.github.com>
Date: Tue, 27 Feb 2024 23:02:39 -0500
Subject: [PATCH] Download energy panel data to CSV (#19863)
* Download energy panel data to CSV
* table format changes
* unique types for cost/compensation
---
src/data/energy.ts | 2 +-
src/panels/energy/ha-panel-energy.ts | 199 ++++++++++++++++++++++++++-
src/translations/en.json | 1 +
3 files changed, 200 insertions(+), 2 deletions(-)
diff --git a/src/data/energy.ts b/src/data/energy.ts
index 32340c0209..a811199cee 100644
--- a/src/data/energy.ts
+++ b/src/data/energy.ts
@@ -781,7 +781,7 @@ export const getEnergyGasUnit = (
: "ft³";
};
-export const getEnergyWaterUnit = (hass: HomeAssistant): string | undefined =>
+export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
hass.config.unit_system.length === "km" ? "L" : "gal";
export const energyStatisticHelpUrl =
diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts
index a9462d0edb..4dd6498ae4 100644
--- a/src/panels/energy/ha-panel-energy.ts
+++ b/src/panels/energy/ha-panel-energy.ts
@@ -7,7 +7,7 @@ import {
html,
nothing,
} from "lit";
-import { mdiPencil } from "@mdi/js";
+import { mdiPencil, mdiDownload } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-menu-button";
import "../../components/ha-list-item";
@@ -19,6 +19,18 @@ import "../lovelace/components/hui-energy-period-selector";
import { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import { navigate } from "../../common/navigate";
+import {
+ getEnergyDataCollection,
+ getEnergyGasUnit,
+ getEnergyWaterUnit,
+ GridSourceTypeEnergyPreference,
+ SolarSourceTypeEnergyPreference,
+ BatterySourceTypeEnergyPreference,
+ GasSourceTypeEnergyPreference,
+ WaterSourceTypeEnergyPreference,
+ DeviceConsumptionEnergyPreference,
+} from "../../data/energy";
+import { fileDownload } from "../../util/file_download";
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
@@ -86,6 +98,15 @@ class PanelEnergy extends LitElement {
${this.hass!.localize("ui.panel.energy.configure")}
+
+
+
+ ${this.hass!.localize("ui.panel.energy.download_data")}
+
`
: nothing}
@@ -122,6 +143,182 @@ class PanelEnergy extends LitElement {
navigate("/config/energy?historyBack=1");
}
+ private async _dumpCSV(ev) {
+ ev.stopPropagation();
+ const energyData = getEnergyDataCollection(this.hass, {
+ key: "energy_dashboard",
+ });
+
+ if (!energyData.prefs || !energyData.state.stats) {
+ return;
+ }
+
+ const gasUnit =
+ getEnergyGasUnit(
+ this.hass,
+ energyData.prefs,
+ energyData.state.statsMetadata
+ ) || "";
+ const waterUnit = getEnergyWaterUnit(this.hass);
+ const electricUnit = "kWh";
+
+ const energy_sources = energyData.prefs.energy_sources;
+ const device_consumption = energyData.prefs.device_consumption;
+ const stats = energyData.state.stats;
+
+ const timeSet = new Set();
+ 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 processStat = function (stat: string, type: string, unit: string) {
+ let n = 0;
+ const row: string[] = [];
+ if (!stats[stat]) {
+ return;
+ }
+ row.push(stat);
+ row.push(type);
+ row.push(unit.normalize("NFKD"));
+ times.forEach((t) => {
+ if (stats[stat][n].start > t) {
+ row.push("");
+ } else if (n < stats[stat].length && stats[stat][n].start === t) {
+ row.push((stats[stat][n].change ?? "").toString());
+ n++;
+ } else {
+ row.push("");
+ }
+ });
+ csv.push(row.join(",") + "\n");
+ };
+
+ const currency = this.hass.config.currency;
+
+ const printCategory = function (
+ type: string,
+ statIds: string[],
+ unit: string,
+ costType?: string
+ ) {
+ if (statIds.length) {
+ statIds.forEach((stat) => processStat(stat, type, unit));
+ if (costType) {
+ statIds.forEach((stat) => {
+ const costStat = energyData.state.info.cost_sensors[stat];
+ if (energyData.state.info.cost_sensors[stat]) {
+ processStat(costStat, costType, currency);
+ }
+ });
+ }
+ }
+ };
+
+ const grid_consumptions: string[] = [];
+ const grid_productions: string[] = [];
+ energy_sources
+ .filter((s) => s.type === "grid")
+ .forEach((source) => {
+ source = source as GridSourceTypeEnergyPreference;
+ source.flow_from.forEach((flowFrom) => {
+ grid_consumptions.push(flowFrom.stat_energy_from);
+ });
+ source.flow_to.forEach((flowTo) => {
+ grid_productions.push(flowTo.stat_energy_to);
+ });
+ });
+
+ printCategory(
+ "grid_consumption",
+ grid_consumptions,
+ electricUnit,
+ "grid_consumption_cost"
+ );
+ printCategory(
+ "grid_return",
+ grid_productions,
+ electricUnit,
+ "grid_return_compensation"
+ );
+
+ 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[] = [];
+ energy_sources
+ .filter((s) => s.type === "gas")
+ .forEach((source) => {
+ source = source as GasSourceTypeEnergyPreference;
+ gas_consumptions.push(source.stat_energy_from);
+ });
+
+ printCategory(
+ "gas_consumption",
+ gas_consumptions,
+ gasUnit,
+ "gas_consumption_cost"
+ );
+
+ const water_consumptions: string[] = [];
+ energy_sources
+ .filter((s) => s.type === "water")
+ .forEach((source) => {
+ source = source as WaterSourceTypeEnergyPreference;
+ water_consumptions.push(source.stat_energy_from);
+ });
+
+ printCategory(
+ "water_consumption",
+ water_consumptions,
+ waterUnit,
+ "water_consumption_cost"
+ );
+
+ const devices: string[] = [];
+ device_consumption.forEach((source) => {
+ source = source as DeviceConsumptionEnergyPreference;
+ devices.push(source.stat_consumption);
+ });
+
+ printCategory("device_consumption", devices, electricUnit);
+
+ const blob = new Blob(csv, {
+ type: "text/csv",
+ });
+ const url = window.URL.createObjectURL(blob);
+ fileDownload(url, "energy.csv");
+ }
+
private _reloadView() {
// Force strategy to be re-run by make a copy of the view
const config = this._lovelace!.config;
diff --git a/src/translations/en.json b/src/translations/en.json
index c5a3c72808..1ad7fed83a 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -6538,6 +6538,7 @@
}
},
"energy": {
+ "download_data": "[%key:ui::panel::history::download_data%]",
"configure": "[%key:ui::dialogs::quick-bar::commands::navigation::energy%]",
"compare": {
"info": "You are comparing the period {start} with the period {end}"