From 32e8c1dc6d331e6666b1a37e8e4fde1fe76d37ae Mon Sep 17 00:00:00 2001 From: Milan Meulemans Date: Tue, 3 Aug 2021 13:00:21 +0200 Subject: [PATCH 1/6] Remove energy currency translation (#9695) --- src/translations/en.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/translations/en.json b/src/translations/en.json index e2439ad3c..13b3db2f9 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -1003,7 +1003,6 @@ "energy": { "caption": "Energy", "description": "Monitor your energy production and consumption", - "currency": "", "grid": { "title": "Electricity grid", "sub": "Configure the amount of energy that you consume from the grid and, if you produce energy, give back to the grid. This allows Home Assistant to track your whole home energy usage.", From 9b33ead8aadf6374ad209422f19e0c54e06b6f7b Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Aug 2021 17:54:10 +0200 Subject: [PATCH 2/6] Final energy tweaks (#9694) --- .../energy/hui-energy-devices-graph-card.ts | 37 ++++++++++++++----- .../hui-energy-solar-consumed-gauge-card.ts | 8 ++-- .../energy/hui-energy-solar-graph-card.ts | 3 ++ .../energy/hui-energy-usage-graph-card.ts | 1 + 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 372f61e60..4451fa046 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -4,6 +4,7 @@ import { ChartOptions, ParsedDataType, } from "chart.js"; +import { getRelativePosition } from "chart.js/helpers"; import { addHours } from "date-fns"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; @@ -11,6 +12,7 @@ import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { getColorByIndex } from "../../../../common/color/colors"; +import { fireEvent } from "../../../../common/dom/fire_event"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import { formatNumber, @@ -120,6 +122,18 @@ export class HuiEnergyDevicesGraphCard }, // @ts-expect-error locale: numberFormatToLocale(this.hass.locale), + onClick: (e: any) => { + const chart = e.chart; + const canvasPosition = getRelativePosition(e, chart); + + const index = Math.abs( + chart.scales.y.getValueForPixel(canvasPosition.y) + ); + fireEvent(this, "hass-more-info", { + // @ts-ignore + entityId: this._chartData?.datasets[0]?.data[index]?.entity_id, + }); + }, }) ); @@ -162,19 +176,13 @@ export class HuiEnergyDevicesGraphCard }, ]; - for (let idx = 0; idx < energyData.prefs.device_consumption.length; idx++) { - const device = energyData.prefs.device_consumption[idx]; + energyData.prefs.device_consumption.forEach((device, idx) => { const entity = this.hass.states[device.stat_consumption]; const label = entity ? computeStateName(entity) : device.stat_consumption; - const color = getColorByIndex(idx); - - borderColor.push(color); - backgroundColor.push(color + "7F"); - const value = - device.stat_consumption in this._data - ? calculateStatisticSumGrowth(this._data[device.stat_consumption]) || + device.stat_consumption in this._data! + ? calculateStatisticSumGrowth(this._data![device.stat_consumption]) || 0 : 0; @@ -182,11 +190,20 @@ export class HuiEnergyDevicesGraphCard // @ts-expect-error y: label, x: value, + entity_id: device.stat_consumption, + idx, }); - } + }); data.sort((a, b) => b.x - a.x); + data.forEach((d: any) => { + const color = getColorByIndex(d.idx); + + borderColor.push(color); + backgroundColor.push(color + "7F"); + }); + this._chartData = { datasets, }; diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts index 3b3fca3be..1ec5343d2 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-consumed-gauge-card.ts @@ -88,10 +88,10 @@ class HuiEnergySolarGaugeCard - This card represents how much of the solar energy was not used by your - home and was returned to the grid. If you frequently return a lot, try - to conserve this energy by installing a battery or buying an electric - car to charge. + This card represents how much of the solar energy was used by your + home and was not returned to the grid. If you frequently produce more + than you consume, try to conserve this energy by installing a battery + or buying an electric car to charge. ${value !== undefined ? html` { if (date in forecastsData!) { diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index ca64a24d6..a8e188bed 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -352,6 +352,7 @@ export class HuiEnergyUsageGraphCard : entity ? computeStateName(entity) : statId, + order: type === "used_solar" ? 0 : idx + 1, borderColor, backgroundColor: hexBlend(borderColor, backgroundColor, 50), stack: "stack", From b246502cb6b5868c3a96b589f54a9d774e97c0f5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Aug 2021 18:50:31 +0200 Subject: [PATCH 3/6] Energy demo (#9698) Co-authored-by: Paulus Schoutsen --- demo/src/configs/demo-configs.ts | 2 + demo/src/ha-demo.ts | 9 + demo/src/stubs/config.ts | 41 +++ demo/src/stubs/energy.ts | 70 +++++ demo/src/stubs/entities.ts | 143 +++++++++++ demo/src/stubs/forecast_solar.ts | 55 ++++ demo/src/stubs/history.ts | 242 ++++++++++++++++++ demo/src/stubs/template.ts | 2 +- src/data/energy.ts | 4 +- src/data/entity_registry.ts | 10 +- src/fake_data/demo_config.ts | 8 +- src/fake_data/demo_panels.ts | 7 + src/fake_data/provide_hass.ts | 13 +- .../energy/hui-energy-devices-graph-card.ts | 2 +- .../energy/hui-energy-distribution-card.ts | 10 +- .../hui-energy-grid-neutrality-gauge-card.ts | 9 +- .../hui-energy-solar-consumed-gauge-card.ts | 7 +- .../energy/hui-energy-solar-graph-card.ts | 2 +- .../energy/hui-energy-usage-graph-card.ts | 2 +- 19 files changed, 613 insertions(+), 25 deletions(-) create mode 100644 demo/src/stubs/config.ts create mode 100644 demo/src/stubs/energy.ts create mode 100644 demo/src/stubs/entities.ts create mode 100644 demo/src/stubs/forecast_solar.ts diff --git a/demo/src/configs/demo-configs.ts b/demo/src/configs/demo-configs.ts index 28176aafa..07dca811f 100644 --- a/demo/src/configs/demo-configs.ts +++ b/demo/src/configs/demo-configs.ts @@ -1,5 +1,6 @@ import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; import { Lovelace } from "../../../src/panels/lovelace/types"; +import { energyEntities } from "../stubs/entities"; import { DemoConfig } from "./types"; export const demoConfigs: Array<() => Promise> = [ @@ -27,6 +28,7 @@ export const setDemoConfig = async ( selectedDemoConfig = confProm; hass.addEntities(config.entities(hass.localize), true); + hass.addEntities(energyEntities()); lovelace.saveConfig(config.lovelace(hass.localize)); hass.mockTheme(config.theme()); }; diff --git a/demo/src/ha-demo.ts b/demo/src/ha-demo.ts index 349968e28..38d348be2 100644 --- a/demo/src/ha-demo.ts +++ b/demo/src/ha-demo.ts @@ -20,6 +20,10 @@ import { mockShoppingList } from "./stubs/shopping_list"; import { mockSystemLog } from "./stubs/system_log"; import { mockTemplate } from "./stubs/template"; import { mockTranslations } from "./stubs/translations"; +import { mockEnergy } from "./stubs/energy"; +import { mockConfig } from "./stubs/config"; +import { energyEntities } from "./stubs/entities"; +import { mockForecastSolar } from "./stubs/forecast_solar"; class HaDemo extends HomeAssistantAppEl { protected async _initializeHass() { @@ -47,8 +51,13 @@ class HaDemo extends HomeAssistantAppEl { mockEvents(hass); mockMediaPlayer(hass); mockFrontend(hass); + mockEnergy(hass); + mockForecastSolar(hass); + mockConfig(hass); mockPersistentNotification(hass); + hass.addEntities(energyEntities()); + // Once config is loaded AND localize, set entities and apply theme. Promise.all([selectedDemoConfig, localizePromise]).then( ([conf, localize]) => { diff --git a/demo/src/stubs/config.ts b/demo/src/stubs/config.ts new file mode 100644 index 000000000..c51700fb1 --- /dev/null +++ b/demo/src/stubs/config.ts @@ -0,0 +1,41 @@ +import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockConfig = (hass: MockHomeAssistant) => { + hass.mockAPI("config/config_entries/entry", () => [ + { + entry_id: "co2signal", + domain: "co2signal", + title: "CO2 Signal", + source: "user", + state: "loaded", + supports_options: false, + supports_unload: true, + pref_disable_new_entities: false, + pref_disable_polling: false, + disabled_by: null, + reason: null, + }, + ]); + hass.mockWS("config/entity_registry/list", () => [ + { + config_entry_id: "co2signal", + device_id: "co2signal", + area_id: null, + disabled_by: null, + entity_id: "sensor.co2_intensity", + name: null, + icon: null, + platform: "co2signal", + }, + { + config_entry_id: "co2signal", + device_id: "co2signal", + area_id: null, + disabled_by: null, + entity_id: "sensor.grid_fossil_fuel_percentage", + name: null, + icon: null, + platform: "co2signal", + }, + ]); +}; diff --git a/demo/src/stubs/energy.ts b/demo/src/stubs/energy.ts new file mode 100644 index 000000000..58b7768b6 --- /dev/null +++ b/demo/src/stubs/energy.ts @@ -0,0 +1,70 @@ +import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockEnergy = (hass: MockHomeAssistant) => { + hass.mockWS("energy/get_prefs", () => ({ + energy_sources: [ + { + type: "grid", + flow_from: [ + { + stat_energy_from: "sensor.energy_consumption_tarif_1", + stat_cost: "sensor.energy_consumption_tarif_1_cost", + entity_energy_from: "sensor.energy_consumption_tarif_1", + entity_energy_price: null, + number_energy_price: null, + }, + { + stat_energy_from: "sensor.energy_consumption_tarif_2", + stat_cost: "sensor.energy_consumption_tarif_2_cost", + entity_energy_from: "sensor.energy_consumption_tarif_2", + entity_energy_price: null, + number_energy_price: null, + }, + ], + flow_to: [ + { + stat_energy_to: "sensor.energy_production_tarif_1", + stat_compensation: "sensor.energy_production_tarif_1_compensation", + entity_energy_to: "sensor.energy_production_tarif_1", + entity_energy_price: null, + number_energy_price: null, + }, + { + stat_energy_to: "sensor.energy_production_tarif_2", + stat_compensation: "sensor.energy_production_tarif_2_compensation", + entity_energy_to: "sensor.energy_production_tarif_2", + entity_energy_price: null, + number_energy_price: null, + }, + ], + cost_adjustment_day: 0, + }, + { + type: "solar", + stat_energy_from: "sensor.solar_production", + config_entry_solar_forecast: ["solar_forecast"], + }, + ], + device_consumption: [ + { + stat_consumption: "sensor.energy_car", + }, + { + stat_consumption: "sensor.energy_ac", + }, + { + stat_consumption: "sensor.energy_washing_machine", + }, + { + stat_consumption: "sensor.energy_dryer", + }, + { + stat_consumption: "sensor.energy_heat_pump", + }, + { + stat_consumption: "sensor.energy_boiler", + }, + ], + })); + hass.mockWS("energy/info", () => ({ cost_sensors: [] })); +}; diff --git a/demo/src/stubs/entities.ts b/demo/src/stubs/entities.ts new file mode 100644 index 000000000..6ebfae96d --- /dev/null +++ b/demo/src/stubs/entities.ts @@ -0,0 +1,143 @@ +import { convertEntities } from "../../../src/fake_data/entity"; + +export const energyEntities = () => + convertEntities({ + "sensor.grid_fossil_fuel_percentage": { + entity_id: "sensor.grid_fossil_fuel_percentage", + state: "88.6", + attributes: { + unit_of_measurement: "%", + }, + }, + "sensor.solar_production": { + entity_id: "sensor.solar_production", + state: "88.6", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Solar", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_consumption_tarif_1": { + entity_id: "sensor.energy_consumption_tarif_1 ", + state: "88.6", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Grid consumption low tariff", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_consumption_tarif_2": { + entity_id: "sensor.energy_consumption_tarif_2", + state: "88.6", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Grid consumption high tariff", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_production_tarif_1": { + entity_id: "sensor.energy_production_tarif_1", + state: "88.6", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Returned to grid low tariff", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_production_tarif_2": { + entity_id: "sensor.energy_production_tarif_2", + state: "88.6", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Returned to grid high tariff", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_consumption_tarif_1_cost": { + entity_id: "sensor.energy_consumption_tarif_1_cost", + state: "2", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + unit_of_measurement: "EUR", + }, + }, + "sensor.energy_consumption_tarif_2_cost": { + entity_id: "sensor.energy_consumption_tarif_2_cost", + state: "2", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + unit_of_measurement: "EUR", + }, + }, + "sensor.energy_production_tarif_1_compensation": { + entity_id: "sensor.energy_production_tarif_1_compensation", + state: "2", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + unit_of_measurement: "EUR", + }, + }, + "sensor.energy_production_tarif_2_compensation": { + entity_id: "sensor.energy_production_tarif_2_compensation", + state: "2", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + unit_of_measurement: "EUR", + }, + }, + "sensor.energy_car": { + entity_id: "sensor.energy_car", + state: "4", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Electric car", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_ac": { + entity_id: "sensor.energy_ac", + state: "3", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Air conditioning", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_washing_machine": { + entity_id: "sensor.energy_washing_machine", + state: "6", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Washing machine", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_dryer": { + entity_id: "sensor.energy_dryer", + state: "5.5", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Dryer", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_heat_pump": { + entity_id: "sensor.energy_heat_pump", + state: "6", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Heat pump", + unit_of_measurement: "kWh", + }, + }, + "sensor.energy_boiler": { + entity_id: "sensor.energy_boiler", + state: "7", + attributes: { + last_reset: "1970-01-01T00:00:00:00+00", + friendly_name: "Boiler", + unit_of_measurement: "kWh", + }, + }, + }); diff --git a/demo/src/stubs/forecast_solar.ts b/demo/src/stubs/forecast_solar.ts new file mode 100644 index 000000000..19bb61083 --- /dev/null +++ b/demo/src/stubs/forecast_solar.ts @@ -0,0 +1,55 @@ +import { format, startOfToday, startOfTomorrow } from "date-fns"; +import { ForecastSolarForecast } from "../../../src/data/forecast_solar"; +import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; + +export const mockForecastSolar = (hass: MockHomeAssistant) => { + const todayString = format(startOfToday(), "yyyy-MM-dd"); + const tomorrowString = format(startOfTomorrow(), "yyyy-MM-dd"); + hass.mockWS( + "forecast_solar/forecasts", + (): Record => ({ + solar_forecast: { + wh_hours: { + [`${todayString}T06:00:00`]: 0, + [`${todayString}T06:23:00`]: 6, + [`${todayString}T06:45:00`]: 39, + [`${todayString}T07:00:00`]: 28, + [`${todayString}T08:00:00`]: 208, + [`${todayString}T09:00:00`]: 352, + [`${todayString}T10:00:00`]: 544, + [`${todayString}T11:00:00`]: 748, + [`${todayString}T12:00:00`]: 1259, + [`${todayString}T13:00:00`]: 1361, + [`${todayString}T14:00:00`]: 1373, + [`${todayString}T15:00:00`]: 1370, + [`${todayString}T16:00:00`]: 1186, + [`${todayString}T17:00:00`]: 937, + [`${todayString}T18:00:00`]: 652, + [`${todayString}T19:00:00`]: 370, + [`${todayString}T20:00:00`]: 155, + [`${todayString}T21:48:00`]: 24, + [`${todayString}T22:36:00`]: 0, + [`${tomorrowString}T06:01:00`]: 0, + [`${tomorrowString}T06:23:00`]: 9, + [`${tomorrowString}T06:45:00`]: 47, + [`${tomorrowString}T07:00:00`]: 48, + [`${tomorrowString}T08:00:00`]: 473, + [`${tomorrowString}T09:00:00`]: 827, + [`${tomorrowString}T10:00:00`]: 1153, + [`${tomorrowString}T11:00:00`]: 1413, + [`${tomorrowString}T12:00:00`]: 1590, + [`${tomorrowString}T13:00:00`]: 1652, + [`${tomorrowString}T14:00:00`]: 1612, + [`${tomorrowString}T15:00:00`]: 1438, + [`${tomorrowString}T16:00:00`]: 1149, + [`${tomorrowString}T17:00:00`]: 830, + [`${tomorrowString}T18:00:00`]: 542, + [`${tomorrowString}T19:00:00`]: 311, + [`${tomorrowString}T20:00:00`]: 140, + [`${tomorrowString}T21:47:00`]: 22, + [`${tomorrowString}T22:34:00`]: 0, + }, + }, + }) + ); +}; diff --git a/demo/src/stubs/history.ts b/demo/src/stubs/history.ts index 5b96b77dd..f5000a70a 100644 --- a/demo/src/stubs/history.ts +++ b/demo/src/stubs/history.ts @@ -1,4 +1,6 @@ +import { addHours, differenceInHours } from "date-fns"; import { HassEntity } from "home-assistant-js-websocket"; +import { StatisticValue } from "../../../src/data/history"; import { MockHomeAssistant } from "../../../src/fake_data/provide_hass"; interface HistoryQueryParams { @@ -64,6 +66,211 @@ const generateHistory = (state, deltas) => { const incrementalUnits = ["clients", "queries", "ads"]; +const generateMeanStatistics = ( + id: string, + start: Date, + end: Date, + initValue: number, + maxDiff: number +) => { + const statistics: StatisticValue[] = []; + let currentDate = new Date(start); + currentDate.setMinutes(0, 0, 0); + let lastVal = initValue; + const now = new Date(); + while (end > currentDate && currentDate < now) { + const delta = Math.random() * maxDiff; + const mean = lastVal + delta; + statistics.push({ + statistic_id: id, + start: currentDate.toISOString(), + mean, + min: mean, + max: mean, + last_reset: "1970-01-01T00:00:00+00:00", + state: mean, + sum: null, + }); + lastVal = mean; + currentDate = addHours(currentDate, 1); + } + return statistics; +}; + +const generateSumStatistics = ( + id: string, + start: Date, + end: Date, + initValue: number, + maxDiff: number +) => { + const statistics: StatisticValue[] = []; + let currentDate = new Date(start); + currentDate.setMinutes(0, 0, 0); + let sum = initValue; + const now = new Date(); + while (end > currentDate && currentDate < now) { + const add = Math.random() * maxDiff; + sum += add; + statistics.push({ + statistic_id: id, + start: currentDate.toISOString(), + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: initValue + sum, + sum, + }); + currentDate = addHours(currentDate, 1); + } + return statistics; +}; + +const generateCurvedStatistics = ( + id: string, + start: Date, + end: Date, + initValue: number, + maxDiff: number, + metered: boolean +) => { + const statistics: StatisticValue[] = []; + let currentDate = new Date(start); + currentDate.setMinutes(0, 0, 0); + let sum = initValue; + const hours = differenceInHours(end, start) - 1; + let i = 0; + let half = false; + const now = new Date(); + while (end > currentDate && currentDate < now) { + const add = Math.random() * maxDiff; + sum += i * add; + statistics.push({ + statistic_id: id, + start: currentDate.toISOString(), + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: initValue + sum, + sum: metered ? sum : null, + }); + currentDate = addHours(currentDate, 1); + if (!half && i > hours / 2) { + half = true; + } + i += half ? -1 : 1; + } + return statistics; +}; + +const statisticsFunctions: Record< + string, + (id: string, start: Date, end: Date) => StatisticValue[] +> = { + "sensor.energy_consumption_tarif_1": (id: string, start: Date, end: Date) => { + const morningEnd = new Date(start.getTime() + 10 * 60 * 60 * 1000); + const morningLow = generateSumStatistics(id, start, morningEnd, 0, 0.7); + const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); + const morningFinalVal = morningLow.length + ? morningLow[morningLow.length - 1].sum! + : 0; + const empty = generateSumStatistics( + id, + morningEnd, + eveningStart, + morningFinalVal, + 0 + ); + const eveningLow = generateSumStatistics( + id, + eveningStart, + end, + morningFinalVal, + 0.7 + ); + return [...morningLow, ...empty, ...eveningLow]; + }, + "sensor.energy_consumption_tarif_2": (id: string, start: Date, end: Date) => { + const morningEnd = new Date(start.getTime() + 9 * 60 * 60 * 1000); + const eveningStart = new Date(start.getTime() + 20 * 60 * 60 * 1000); + const highTarif = generateSumStatistics( + id, + morningEnd, + eveningStart, + 0, + 0.3 + ); + const highTarifFinalVal = highTarif.length + ? highTarif[highTarif.length - 1].sum! + : 0; + const morning = generateSumStatistics(id, start, morningEnd, 0, 0); + const evening = generateSumStatistics( + id, + eveningStart, + end, + highTarifFinalVal, + 0 + ); + return [...morning, ...highTarif, ...evening]; + }, + "sensor.energy_production_tarif_1": (id, start, end) => + generateSumStatistics(id, start, end, 0, 0), + "sensor.energy_production_tarif_1_compensation": (id, start, end) => + generateSumStatistics(id, start, end, 0, 0), + "sensor.energy_production_tarif_2": (id, start, end) => { + const productionStart = new Date(start.getTime() + 9 * 60 * 60 * 1000); + const productionEnd = new Date(start.getTime() + 21 * 60 * 60 * 1000); + const production = generateCurvedStatistics( + id, + productionStart, + productionEnd, + 0, + 0.15, + true + ); + const productionFinalVal = production.length + ? production[production.length - 1].sum! + : 0; + const morning = generateSumStatistics(id, start, productionStart, 0, 0); + const evening = generateSumStatistics( + id, + productionEnd, + end, + productionFinalVal, + 0 + ); + return [...morning, ...production, ...evening]; + }, + "sensor.solar_production": (id, start, end) => { + const productionStart = new Date(start.getTime() + 7 * 60 * 60 * 1000); + const productionEnd = new Date(start.getTime() + 23 * 60 * 60 * 1000); + const production = generateCurvedStatistics( + id, + productionStart, + productionEnd, + 0, + 0.3, + true + ); + const productionFinalVal = production.length + ? production[production.length - 1].sum! + : 0; + const morning = generateSumStatistics(id, start, productionStart, 0, 0); + const evening = generateSumStatistics( + id, + productionEnd, + end, + productionFinalVal, + 0 + ); + return [...morning, ...production, ...evening]; + }, + "sensor.grid_fossil_fuel_percentage": (id, start, end) => + generateMeanStatistics(id, start, end, 35, 1.3), +}; + export const mockHistory = (mockHass: MockHomeAssistant) => { mockHass.mockAPI( new RegExp("history/period/.+"), @@ -133,4 +340,39 @@ export const mockHistory = (mockHass: MockHomeAssistant) => { return results; } ); + mockHass.mockWS( + "history/statistics_during_period", + ({ statistic_ids, start_time, end_time }, hass) => { + const start = new Date(start_time); + const end = new Date(end_time); + + const statistics: Record = {}; + + statistic_ids.forEach((id: string) => { + if (id in statisticsFunctions) { + statistics[id] = statisticsFunctions[id](id, start, end); + } else { + const entityState = hass.states[id]; + const state = entityState ? Number(entityState.state) : 1; + statistics[id] = + entityState && "last_reset" in entityState.attributes + ? generateSumStatistics( + id, + start, + end, + state, + state * (state > 80 ? 0.01 : 0.05) + ) + : generateMeanStatistics( + id, + start, + end, + state, + state * (state > 80 ? 0.05 : 0.1) + ); + } + }); + return statistics; + } + ); }; diff --git a/demo/src/stubs/template.ts b/demo/src/stubs/template.ts index 0e3c1a263..fc47a0839 100644 --- a/demo/src/stubs/template.ts +++ b/demo/src/stubs/template.ts @@ -6,7 +6,7 @@ export const mockTemplate = (hass: MockHomeAssistant) => { body: { message: "Template dev tool does not work in the demo." }, }) ); - hass.mockWS("render_template", (msg, onChange) => { + hass.mockWS("render_template", (msg, _hass, onChange) => { onChange!({ result: msg.template, listeners: { all: false, domains: [], entities: [], time: false }, diff --git a/src/data/energy.ts b/src/data/energy.ts index 4bb9611b0..74bec17db 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -228,7 +228,7 @@ const getEnergyData = async ( const stats = await fetchStatistics(hass!, addHours(start, -1), end, statIDs); // Subtract 1 hour from start to get starting point data - return { + const data = { start, end, info, @@ -237,6 +237,8 @@ const getEnergyData = async ( co2SignalConfigEntry, co2SignalEntity, }; + + return data; }; export interface EnergyCollection extends Collection { diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts index 0bf6a5b25..28f389922 100644 --- a/src/data/entity_registry.ts +++ b/src/data/entity_registry.ts @@ -1,4 +1,5 @@ import { Connection, createCollection } from "home-assistant-js-websocket"; +import { Store } from "home-assistant-js-websocket/dist/store"; import { computeStateName } from "../common/entity/compute_state_name"; import { debounce } from "../common/util/debounce"; import { HomeAssistant } from "../types"; @@ -96,12 +97,15 @@ export const removeEntityRegistryEntry = ( entity_id: entityId, }); -export const fetchEntityRegistry = (conn) => - conn.sendMessagePromise({ +export const fetchEntityRegistry = (conn: Connection) => + conn.sendMessagePromise({ type: "config/entity_registry/list", }); -const subscribeEntityRegistryUpdates = (conn, store) => +const subscribeEntityRegistryUpdates = ( + conn: Connection, + store: Store +) => conn.subscribeEvents( debounce( () => diff --git a/src/fake_data/demo_config.ts b/src/fake_data/demo_config.ts index 91052868e..b1a4d9837 100644 --- a/src/fake_data/demo_config.ts +++ b/src/fake_data/demo_config.ts @@ -11,7 +11,13 @@ export const demoConfig: HassConfig = { temperature: "°C", volume: "L", }, - components: ["notify.html5", "history", "shopping_list"], + components: [ + "notify.html5", + "history", + "shopping_list", + "forecast_solar", + "energy", + ], time_zone: "America/Los_Angeles", config_dir: "/config", version: "DEMO", diff --git a/src/fake_data/demo_panels.ts b/src/fake_data/demo_panels.ts index 71aa6ac0f..319aa4997 100644 --- a/src/fake_data/demo_panels.ts +++ b/src/fake_data/demo_panels.ts @@ -72,6 +72,13 @@ export const demoPanels: Panels = { config: null, url_path: "map", }, + energy: { + component_name: "energy", + icon: "hass:lightning-bolt", + title: "energy", + config: null, + url_path: "energy", + }, // config: { // component_name: "config", // icon: "hass:cog", diff --git a/src/fake_data/provide_hass.ts b/src/fake_data/provide_hass.ts index d537ffa80..e90665542 100644 --- a/src/fake_data/provide_hass.ts +++ b/src/fake_data/provide_hass.ts @@ -33,7 +33,11 @@ export interface MockHomeAssistant extends HomeAssistant { addTranslations(translations: Record, language?: string); mockWS( type: string, - callback: (msg: any, onChange?: (response: any) => void) => any + callback: ( + msg: any, + hass: MockHomeAssistant, + onChange?: (response: any) => void + ) => any ); mockAPI(path: string | RegExp, callback: MockRestCallback); mockEvent(event); @@ -144,7 +148,7 @@ export const provideHass = ( const callback = wsCommands[msg.type]; if (callback) { - callback(msg); + callback(msg, hass()); } else { // eslint-disable-next-line console.error(`Unknown WS command: ${msg.type}`); @@ -153,7 +157,7 @@ export const provideHass = ( sendMessagePromise: async (msg) => { const callback = wsCommands[msg.type]; return callback - ? callback(msg) + ? callback(msg, hass()) : Promise.reject({ code: "command_not_mocked", message: `WS Command ${msg.type} is not implemented in provide_hass.`, @@ -162,7 +166,7 @@ export const provideHass = ( subscribeMessage: async (onChange, msg) => { const callback = wsCommands[msg.type]; return callback - ? callback(msg, onChange) + ? callback(msg, hass(), onChange) : Promise.reject({ code: "command_not_mocked", message: `WS Command ${msg.type} is not implemented in provide_hass.`, @@ -266,6 +270,7 @@ export const provideHass = ( updateStates, updateTranslations, addTranslations, + loadFragmentTranslation: async (_fragment: string) => hass().localize, addEntities, mockWS(type, callback) { wsCommands[type] = callback; diff --git a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts index 4451fa046..3fafe1ad1 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-devices-graph-card.ts @@ -153,7 +153,7 @@ export class HuiEnergyDevicesGraphCard endTime = new Date( Math.max( ...statisticsData.map((stats) => - new Date(stats[stats.length - 1].start).getTime() + stats.length ? new Date(stats[stats.length - 1].start).getTime() : 0 ) ) ); 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 3fef71602..6e233c50f 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -10,7 +10,6 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, html, LitElement, svg } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { ifDefined } from "lit/directives/if-defined"; import "@material/mwc-button"; import { formatNumber } from "../../../../common/string/format_number"; import "../../../../components/ha-card"; @@ -122,7 +121,8 @@ class HuiEnergyDistrubutionCard let homeLowCarbonCircumference: number | undefined; let homeHighCarbonCircumference: number | undefined; - let electricityMapUrl: string | undefined; + // This fallback is used in the demo + let electricityMapUrl = "https://www.electricitymap.org"; if ( this._data.co2SignalEntity && @@ -140,8 +140,8 @@ class HuiEnergyDistrubutionCard const co2State = this.hass.states[this._data.co2SignalEntity]; - if (co2State) { - electricityMapUrl = `https://www.electricitymap.org/zone/${co2State.attributes.country_code}`; + if (co2State?.attributes.country_code) { + electricityMapUrl += `/zone/${co2State.attributes.country_code}`; } if (highCarbonConsumption !== null) { @@ -168,7 +168,7 @@ class HuiEnergyDistrubutionCard Non-fossil diff --git a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts index dd68c567e..00609e178 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-grid-neutrality-gauge-card.ts @@ -98,10 +98,11 @@ class HuiEnergyGridGaugeCard - This card represents your energy dependency. If it's green, it means - you produced more energy than that you consumed from the grid. If it's - in the red, it means that you relied on the grid for part of your - home's energy consumption. + This card represents your energy dependency. +

+ If it's green, it means you produced more energy than that you + consumed from the grid. If it's in the red, it means that you relied + on the grid for part of your home's energy consumption.
${value !== undefined ? html` This card represents how much of the solar energy was used by your - home and was not returned to the grid. If you frequently produce more - than you consume, try to conserve this energy by installing a battery - or buying an electric car to charge. + home and was not returned to the grid. +

+ If you frequently produce more than you consume, try to conserve this + energy by installing a battery or buying an electric car to charge.
${value !== undefined ? html` - new Date(stats[stats.length - 1].start).getTime() + stats.length ? new Date(stats[stats.length - 1].start).getTime() : 0 ) ) ); diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index a8e188bed..a87bfdc52 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -243,7 +243,7 @@ export class HuiEnergyUsageGraphCard endTime = new Date( Math.max( ...statisticsData.map((stats) => - new Date(stats[stats.length - 1].start).getTime() + stats.length ? new Date(stats[stats.length - 1].start).getTime() : 0 ) ) ); From d699647418b19948994b5f143998cd1bdd85234c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Aug 2021 23:19:26 +0200 Subject: [PATCH 4/6] Fix calculate sum growth + refreshing too often (#9699) --- src/data/energy.ts | 14 +- src/data/history.ts | 124 ++------ test-mocha/data/history.spec.ts | 503 +++++++++++++++++++++++++++++++- 3 files changed, 538 insertions(+), 103 deletions(-) diff --git a/src/data/energy.ts b/src/data/energy.ts index 74bec17db..40a024995 100644 --- a/src/data/energy.ts +++ b/src/data/energy.ts @@ -302,10 +302,10 @@ export const getEnergyDataCollection = ( // 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) { + if (nextFetch.getMinutes() >= 20) { nextFetch.setHours(nextFetch.getHours() + 1); } - nextFetch.setMinutes(20); + nextFetch.setMinutes(20, 0, 0); collection._refreshTimeout = window.setTimeout( () => collection.refresh(), @@ -362,15 +362,15 @@ export const getEnergyDataCollection = ( collection.setPeriod = (newStart: Date, newEnd?: Date) => { collection.start = newStart; collection.end = newEnd; - if (collection._updatePeriodTimeout) { - clearTimeout(collection._updatePeriodTimeout); - collection._updatePeriodTimeout = undefined; - } if ( collection.start.getTime() === startOfToday().getTime() && - collection.end?.getTime() === endOfToday().getTime() + collection.end?.getTime() === endOfToday().getTime() && + !collection._updatePeriodTimeout ) { scheduleUpdatePeriod(); + } else if (collection._updatePeriodTimeout) { + clearTimeout(collection._updatePeriodTimeout); + collection._updatePeriodTimeout = undefined; } }; return collection; diff --git a/src/data/history.ts b/src/data/history.ts index 481ddf779..b1e1b9f82 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -346,64 +346,32 @@ export const statisticsHaveType = ( 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; +// Merge the growth of multiple sum statistics into one +const mergeSumGrowthStatistics = (stats: StatisticValue[][]) => { + const result = {}; - for (const stat of stats) { + stats.forEach((stat) => { if (stat.length === 0) { - continue; + return; } - 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; + let prevSum: number | null = null; + stat.forEach((statVal) => { + if (statVal.sum === null) { + return; } - if (stat[0].start !== earliestStart) { - continue; + if (prevSum === null) { + prevSum = statVal.sum; + return; } - const statVal = stat.shift()!; - if (!statVal.sum) { - continue; + const growth = statVal.sum - prevSum; + if (statVal.start in result) { + result[statVal.start] += growth; + } else { + result[statVal.start] = growth; } - sum += statVal.sum; - } - - result.push({ - start: earliestStart, - sum, + prevSum = statVal.sum; }); - } + }); return result; }; @@ -418,55 +386,23 @@ export const calculateStatisticsSumGrowthWithPercentage = ( ): number | null => { let sum: number | null = null; - if (sumStats.length === 0) { + if (sumStats.length === 0 || percentageStat.length === 0) { return null; } - const sumStatsToProcess = mergeSumStatistics(sumStats); - const percentageStatToProcess = [...percentageStat]; + const sumGrowthToProcess = mergeSumGrowthStatistics(sumStats); - 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; + percentageStat.forEach((percentageStatValue) => { + const sumGrowth = sumGrowthToProcess[percentageStatValue.start]; + if (sumGrowth === undefined) { + return; } - lastSum = stat.sum; - } - - while (percentageStatToProcess.length > 0) { - if (!sumStatsToProcess.length) { - return sum; + if (sum === null) { + sum = sumGrowth * (percentageStatValue.mean! / 100); + } else { + sum += sumGrowth * (percentageStatValue.mean! / 100); } - - // 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/test-mocha/data/history.spec.ts b/test-mocha/data/history.spec.ts index 8ba9efd65..6106040d7 100644 --- a/test-mocha/data/history.spec.ts +++ b/test-mocha/data/history.spec.ts @@ -10,7 +10,81 @@ describe("calculateStatisticsSumGrowthWithPercentage", () => { ); }); - it("Returns null if not enough values", async () => { + it("Returns null if not enough sum stat 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, + }, + ], + [] + ), + null + ); + }); + + it("Returns null if not enough percentage stat values", async () => { + assert.strictEqual( + calculateStatisticsSumGrowthWithPercentage( + [], + [ + [ + { + 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, + }, + ], + ] + ), + null + ); + }); + + it("Returns a percentage of the growth", async () => { assert.strictEqual( calculateStatisticsSumGrowthWithPercentage( [ @@ -68,10 +142,435 @@ describe("calculateStatisticsSumGrowthWithPercentage", () => { state: null, }, ], - [], + [ + { + statistic_id: "sensor.off_peak_consumption", + start: "2021-07-28T04:00:00Z", + last_reset: null, + max: null, + mean: null, + min: null, + sum: 50, + state: null, + }, + { + statistic_id: "sensor.off_peak_consumption", + start: "2021-07-28T05:00:00Z", + last_reset: null, + max: null, + mean: null, + min: null, + sum: 100, + state: null, + }, + { + statistic_id: "sensor.off_peak_consumption", + start: "2021-07-28T07:00:00Z", + last_reset: null, + max: null, + mean: null, + min: null, + sum: 200, + state: null, + }, + ], + ] + ), + 200 + ); + }); + + it("It ignores sum data that doesnt match start", 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-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 ); }); + + it("It ignores percentage data that doesnt match start", async () => { + assert.strictEqual( + calculateStatisticsSumGrowthWithPercentage( + [ + { + statistic_id: "sensor.carbon_intensity", + start: "2021-07-28T04:00:00Z", + last_reset: null, + max: 25, + mean: 25, + min: 25, + sum: null, + state: null, + }, + { + 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 + ); + }); + + it("Returns a percentage of the growth", async () => { + assert.strictEqual( + calculateStatisticsSumGrowthWithPercentage( + [ + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T06:00:00.000Z", + mean: 10, + min: 10, + max: 10, + last_reset: "1970-01-01T00:00:00+00:00", + state: 10, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T07:00:00.000Z", + mean: 20, + min: 20, + max: 20, + last_reset: "1970-01-01T00:00:00+00:00", + state: 20, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T08:00:00.000Z", + mean: 30, + min: 30, + max: 30, + last_reset: "1970-01-01T00:00:00+00:00", + state: 30, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T09:00:00.000Z", + mean: 40, + min: 40, + max: 40, + last_reset: "1970-01-01T00:00:00+00:00", + state: 40, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T10:00:00.000Z", + mean: 50, + min: 50, + max: 50, + last_reset: "1970-01-01T00:00:00+00:00", + state: 50, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T11:00:00.000Z", + mean: 60, + min: 60, + max: 60, + last_reset: "1970-01-01T00:00:00+00:00", + state: 60, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T12:00:00.000Z", + mean: 70, + min: 70, + max: 70, + last_reset: "1970-01-01T00:00:00+00:00", + state: 70, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T13:00:00.000Z", + mean: 80, + min: 80, + max: 80, + last_reset: "1970-01-01T00:00:00+00:00", + state: 80, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T14:00:00.000Z", + mean: 90, + min: 90, + max: 90, + last_reset: "1970-01-01T00:00:00+00:00", + state: 90, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T15:00:00.000Z", + mean: 100, + min: 100, + max: 100, + last_reset: "1970-01-01T00:00:00+00:00", + state: 100, + sum: null, + }, + { + statistic_id: "sensor.grid_fossil_fuel_percentage", + start: "2021-08-03T16:00:00.000Z", + mean: 110, + min: 110, + max: 110, + last_reset: "1970-01-01T00:00:00+00:00", + state: 120, + sum: null, + }, + ], + [ + [ + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T06:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 10, + sum: 10, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T07:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 20, + sum: 20, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T08:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 30, + sum: 30, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T09:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 40, + sum: 40, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T10:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 50, + sum: 50, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T11:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 60, + sum: 60, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T12:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 70, + sum: 70, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T13:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 80, + sum: 80, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T14:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 90, + sum: 90, + }, + { + statistic_id: "sensor.energy_consumption_tarif_1", + start: "2021-08-03T15:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 100, + sum: 100, + }, + ], + [ + { + statistic_id: "sensor.energy_consumption_tarif_2", + start: "2021-08-03T15:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 10, + sum: 10, + }, + { + statistic_id: "sensor.energy_consumption_tarif_2", + start: "2021-08-03T16:00:00.000Z", + mean: null, + min: null, + max: null, + last_reset: "1970-01-01T00:00:00+00:00", + state: 20, + sum: 20, + }, + ], + ] + ), + 65 + ); + }); }); From 5ba24211e22ea7aa18bd9a83189ccc48da5483a5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Aug 2021 23:22:32 +0200 Subject: [PATCH 5/6] Add messages when no data (#9700) * Add messages when no data * 2 hours * comment --- src/panels/config/energy/ha-config-energy.ts | 8 +- src/panels/energy/ha-panel-energy.ts | 10 ++ .../energy/hui-energy-solar-graph-card.ts | 97 ++++++++++++------- .../energy/hui-energy-usage-graph-card.ts | 22 ++++- .../components/hui-energy-period-selector.ts | 10 +- 5 files changed, 100 insertions(+), 47 deletions(-) diff --git a/src/panels/config/energy/ha-config-energy.ts b/src/panels/config/energy/ha-config-energy.ts index a15a6c417..fab928a23 100644 --- a/src/panels/config/energy/ha-config-energy.ts +++ b/src/panels/config/energy/ha-config-energy.ts @@ -64,6 +64,12 @@ class HaConfigEnergy extends LitElement { .route=${this.route} .tabs=${configSections.experiences} > + +
+ After setting up a new device, it can take up to 2 hours for new + data to arrive in your energy dashboard. +
+
@@ -123,6 +132,7 @@ class PanelEnergy extends LitElement { hui-energy-period-selector { width: 100%; padding-left: 16px; + --disabled-text-color: rgba(var(--rgb-text-primary-color), 0.5); } `, ]; diff --git a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts index 955988159..875845f10 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-solar-graph-card.ts @@ -4,8 +4,13 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket"; import memoizeOne from "memoize-one"; import { classMap } from "lit/directives/class-map"; import "../../../../components/ha-card"; -import { ChartData, ChartDataset, ChartOptions } from "chart.js"; -import { endOfToday, startOfToday } from "date-fns"; +import { + ChartData, + ChartDataset, + ChartOptions, + ScatterDataPoint, +} from "chart.js"; +import { endOfToday, isToday, startOfToday } from "date-fns"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergySolarGraphCardConfig } from "../types"; @@ -28,8 +33,6 @@ import { } from "../../../../data/forecast_solar"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import "../../../../components/chart/ha-chart-base"; -import "../../../../components/ha-switch"; -import "../../../../components/ha-formfield"; import { formatNumber, numberFormatToLocale, @@ -50,8 +53,6 @@ export class HuiEnergySolarGraphCard datasets: [], }; - @state() private _forecasts?: Record; - @state() private _start = startOfToday(); @state() private _end = endOfToday(); @@ -96,6 +97,13 @@ export class HuiEnergySolarGraphCard )} chart-type="bar" > + ${!this._chartData.datasets.length + ? html`
+ ${isToday(this._start) + ? "There is no data to show. It can take up to 2 hours for new data to arrive after you configure your energy dashboard." + : "There is not data for this period."} +
` + : ""}
`; @@ -188,11 +196,12 @@ export class HuiEnergySolarGraphCard (source) => source.type === "solar" ) as SolarSourceTypeEnergyPreference[]; + let forecasts: Record; if ( isComponentLoaded(this.hass, "forecast_solar") && solarSources.some((source) => source.config_entry_solar_forecast) ) { - this._forecasts = await getForecastSolarForecasts(this.hass); + forecasts = await getForecastSolarForecasts(this.hass); } const statisticsData = Object.values(energyData.stats); @@ -225,18 +234,11 @@ export class HuiEnergySolarGraphCard ? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx))) : solarColor; - data.push({ - label: `Production ${ - entity ? computeStateName(entity) : source.stat_energy_from - }`, - borderColor, - backgroundColor: borderColor + "7F", - data: [], - }); - let prevValue: number | null = null; let prevStart: string | null = null; + const solarProductionData: ScatterDataPoint[] = []; + // Process solar production data. if (energyData.stats[source.stat_energy_from]) { for (const point of energyData.stats[source.stat_energy_from]) { @@ -252,7 +254,7 @@ export class HuiEnergySolarGraphCard } const value = point.sum - prevValue; const date = new Date(point.start); - data[0].data.push({ + solarProductionData.push({ x: date.getTime(), y: value, }); @@ -261,7 +263,16 @@ export class HuiEnergySolarGraphCard } } - const forecasts = this._forecasts; + if (solarProductionData.length) { + data.push({ + label: `Production ${ + entity ? computeStateName(entity) : source.stat_energy_from + }`, + borderColor, + backgroundColor: borderColor + "7F", + data: solarProductionData, + }); + } // Process solar forecast data. if (forecasts && source.config_entry_solar_forecast) { @@ -286,22 +297,7 @@ export class HuiEnergySolarGraphCard }); if (forecastsData) { - const forecast: ChartDataset<"line"> = { - type: "line", - label: `Forecast ${ - entity ? computeStateName(entity) : source.stat_energy_from - }`, - fill: false, - stepped: false, - borderColor: computedStyles.getPropertyValue( - "--primary-text-color" - ), - borderDash: [7, 5], - pointRadius: 0, - data: [], - }; - data.push(forecast); - + const solarForecastData: ScatterDataPoint[] = []; for (const [date, value] of Object.entries(forecastsData)) { const dateObj = new Date(date); if ( @@ -310,11 +306,28 @@ export class HuiEnergySolarGraphCard ) { continue; } - forecast.data.push({ + solarForecastData.push({ x: dateObj.getTime(), y: value / 1000, }); } + + if (solarForecastData.length) { + data.push({ + type: "line", + label: `Forecast ${ + entity ? computeStateName(entity) : source.stat_energy_from + }`, + fill: false, + stepped: false, + borderColor: computedStyles.getPropertyValue( + "--primary-text-color" + ), + borderDash: [7, 5], + pointRadius: 0, + data: solarForecastData, + }); + } } } @@ -344,8 +357,18 @@ export class HuiEnergySolarGraphCard .has-header { padding-top: 0; } - ha-formfield { - margin-bottom: 16px; + .no-data { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + padding: 20%; + margin-left: 32px; + box-sizing: border-box; } `; } diff --git a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts index a87bfdc52..7452d36d2 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts @@ -1,5 +1,5 @@ import { ChartData, ChartDataset, ChartOptions } from "chart.js"; -import { startOfToday, endOfToday } from "date-fns"; +import { startOfToday, endOfToday, isToday } from "date-fns"; import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -84,6 +84,13 @@ export class HuiEnergyUsageGraphCard )} chart-type="bar" > + ${!this._chartData.datasets.length + ? html`
+ ${isToday(this._start) + ? "There is no data to show. It can take up to 2 hours for new data to arrive after you configure your energy dashboard." + : "There is not data for this period."} +
` + : ""}
`; @@ -394,6 +401,19 @@ export class HuiEnergyUsageGraphCard .has-header { padding-top: 0; } + .no-data { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + display: flex; + justify-content: center; + align-items: center; + padding: 20%; + margin-left: 32px; + box-sizing: border-box; + } `; } } diff --git a/src/panels/lovelace/components/hui-energy-period-selector.ts b/src/panels/lovelace/components/hui-energy-period-selector.ts index ca587738b..fa82e1d3c 100644 --- a/src/panels/lovelace/components/hui-energy-period-selector.ts +++ b/src/panels/lovelace/components/hui-energy-period-selector.ts @@ -104,14 +104,8 @@ export class HuiEnergyPeriodSelector extends SubscribeMixin(LitElement) { --mdc-theme-primary: currentColor; --mdc-button-outline-color: currentColor; - --mdc-button-disabled-outline-color: rgba( - var(--rgb-text-primary-color), - 0.5 - ); - --mdc-button-disabled-ink-color: rgba( - var(--rgb-text-primary-color), - 0.5 - ); + --mdc-button-disabled-outline-color: var(--disabled-text-color); + --mdc-button-disabled-ink-color: var(--disabled-text-color); } `; } From 37f1bd7d63ff7bd4132f7ab83194602c5430d1fe Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Aug 2021 23:22:46 +0200 Subject: [PATCH 6/6] Bumped version to 20210803.1 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 5f057d036..9c988b0d7 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20210803.0", + version="20210803.1", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors",