diff --git a/setup.py b/setup.py index 758a5198f2..2066310b3d 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20210730.0", + version="20210801.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/frontend", author="The Home Assistant Authors", diff --git a/src/common/string/format_number.ts b/src/common/string/format_number.ts index 01279cb715..56ab2ec8dc 100644 --- a/src/common/string/format_number.ts +++ b/src/common/string/format_number.ts @@ -78,7 +78,10 @@ const getDefaultFormatOptions = ( num: string | number, options?: Intl.NumberFormatOptions ): Intl.NumberFormatOptions => { - const defaultOptions: Intl.NumberFormatOptions = options || {}; + const defaultOptions: Intl.NumberFormatOptions = { + maximumFractionDigits: 2, + ...options, + }; if (typeof num !== "string") { return defaultOptions; diff --git a/src/components/chart/ha-chart-base.ts b/src/components/chart/ha-chart-base.ts index 2ff0550efa..36970c31a3 100644 --- a/src/components/chart/ha-chart-base.ts +++ b/src/components/chart/ha-chart-base.ts @@ -29,9 +29,11 @@ export default class HaChartBase extends LitElement { @property({ attribute: false }) public plugins?: any[]; - @state() private _tooltip?: Tooltip; + @property({ type: Number }) public height?: number; - @state() private _height?: string; + @state() private _chartHeight?: number; + + @state() private _tooltip?: Tooltip; @state() private _hiddenDatasets: Set = new Set(); @@ -96,11 +98,8 @@ export default class HaChartBase extends LitElement {
@@ -194,7 +193,7 @@ export default class HaChartBase extends LitElement { { id: "afterRenderHook", afterRender: (chart) => { - this._height = `${chart.height}px`; + this._chartHeight = chart.height; }, legend: { ...this.options?.plugins?.legend, @@ -255,8 +254,8 @@ export default class HaChartBase extends LitElement { height: 0; transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); } - :host(:not([chart-type="timeline"])) canvas { - max-height: 400px; + canvas { + max-height: var(--chart-max-height, 400px); } .chartLegend { text-align: center; diff --git a/src/components/chart/state-history-chart-line.ts b/src/components/chart/state-history-chart-line.ts index b2bddfba4f..574d86112a 100644 --- a/src/components/chart/state-history-chart-line.ts +++ b/src/components/chart/state-history-chart-line.ts @@ -2,7 +2,10 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { html, LitElement, PropertyValues } from "lit"; import { property, state } from "lit/decorators"; import { getColorByIndex } from "../../common/color/colors"; -import { numberFormatToLocale } from "../../common/string/format_number"; +import { + formatNumber, + numberFormatToLocale, +} from "../../common/string/format_number"; import { LineChartEntity, LineChartState } from "../../data/history"; import { HomeAssistant } from "../../types"; import "./ha-chart-base"; @@ -85,7 +88,10 @@ class StateHistoryChartLine extends LitElement { mode: "nearest", callbacks: { label: (context) => - `${context.dataset.label}: ${context.parsed.y} ${this.unit}`, + `${context.dataset.label}: ${formatNumber( + context.parsed.y, + this.hass.locale + )} ${this.unit}`, }, }, filler: { diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts index db81411b6a..500e90a342 100644 --- a/src/components/chart/state-history-chart-timeline.ts +++ b/src/components/chart/state-history-chart-timeline.ts @@ -1,6 +1,6 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js"; import { HassEntity } from "home-assistant-js-websocket"; -import { html, LitElement, PropertyValues } from "lit"; +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; import { getColorByIndex } from "../../common/color/colors"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; @@ -99,6 +99,7 @@ export class StateHistoryChartTimeline extends LitElement { `; @@ -304,6 +305,14 @@ export class StateHistoryChartTimeline extends LitElement { datasets: datasets, }; } + + static get styles(): CSSResultGroup { + return css` + ha-chart-base { + --chart-max-height: none; + } + `; + } } declare global { diff --git a/src/components/chart/statistics-chart.ts b/src/components/chart/statistics-chart.ts index 68431696ba..00766f477e 100644 --- a/src/components/chart/statistics-chart.ts +++ b/src/components/chart/statistics-chart.ts @@ -16,10 +16,15 @@ import { customElement, property, state } from "lit/decorators"; import { getColorByIndex } from "../../common/color/colors"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { computeStateName } from "../../common/entity/compute_state_name"; -import { numberFormatToLocale } from "../../common/string/format_number"; import { + formatNumber, + numberFormatToLocale, +} from "../../common/string/format_number"; +import { + getStatisticIds, Statistics, statisticsHaveType, + StatisticsMetaData, StatisticType, } from "../../data/history"; import type { HomeAssistant } from "../../types"; @@ -31,8 +36,12 @@ class StatisticsChart extends LitElement { @property({ attribute: false }) public statisticsData!: Statistics; + @property({ type: Array }) public statisticIds?: StatisticsMetaData[]; + @property() public names: boolean | Record = false; + @property() public unit?: string; + @property({ attribute: false }) public endTime?: Date; @property({ type: Array }) public statTypes: Array = [ @@ -46,12 +55,12 @@ class StatisticsChart extends LitElement { @property({ type: Boolean }) public isLoadingData = false; - @state() private _chartData?: ChartData; + @state() private _chartData: ChartData = { datasets: [] }; @state() private _chartOptions?: ChartOptions; protected shouldUpdate(changedProps: PropertyValues): boolean { - return !(changedProps.size === 1 && changedProps.has("hass")); + return changedProps.size > 1 || !changedProps.has("hass"); } public willUpdate(changedProps: PropertyValues) { @@ -124,23 +133,35 @@ class StatisticsChart extends LitElement { }, }, y: { + beginAtZero: false, ticks: { maxTicksLimit: 7, }, + title: { + display: this.unit, + text: this.unit, + }, }, }, plugins: { tooltip: { mode: "nearest", callbacks: { - label: (context) => `${context.dataset.label}: ${context.parsed.y}`, + label: (context) => + `${context.dataset.label}: ${formatNumber( + context.parsed.y, + this.hass.locale + )} ${ + // @ts-ignore + context.dataset.unit || "" + }`, }, }, filler: { propagate: true, }, legend: { - display: false, + display: true, labels: { usePointStyle: true, }, @@ -154,6 +175,7 @@ class StatisticsChart extends LitElement { tension: 0.4, borderWidth: 1.5, }, + bar: { borderWidth: 1.5, borderRadius: 4 }, point: { hitRadius: 5, }, @@ -163,10 +185,19 @@ class StatisticsChart extends LitElement { }; } - private _generateData() { + private async _getStatisticIds() { + this.statisticIds = await getStatisticIds(this.hass); + } + + private async _generateData() { if (!this.statisticsData) { return; } + + if (!this.statisticIds) { + await this._getStatisticIds(); + } + let colorIndex = 0; const statisticsData = Object.values(this.statisticsData); const totalDataSets: ChartDataset<"line">[] = []; @@ -191,6 +222,8 @@ class StatisticsChart extends LitElement { endTime = new Date(); } + let unit: string | undefined | null; + const names = this.names || {}; statisticsData.forEach((stats) => { const firstStat = stats[0]; @@ -203,6 +236,19 @@ class StatisticsChart extends LitElement { name = firstStat.statistic_id; } } + + const meta = this.statisticIds!.find( + (stat) => stat.statistic_id === firstStat.statistic_id + ); + + if (!this.unit) { + if (unit === undefined) { + unit = meta?.unit_of_measurement; + } else if (unit !== meta?.unit_of_measurement) { + unit = null; + } + } + // array containing [value1, value2, etc] let prevValues: Array | null = null; @@ -237,64 +283,54 @@ class StatisticsChart extends LitElement { const color = getColorByIndex(colorIndex); colorIndex++; - const addDataSet = ( - nameY: string, - borderColor: string, - backgroundColor: string, - step = false, - fill?: boolean | number | string - ) => { - statDataSets.push({ - label: nameY, - fill: fill || false, - borderColor, - backgroundColor: backgroundColor, - stepped: step ? "before" : false, - pointRadius: 0, - data: [], - }); - }; - const statTypes: this["statTypes"] = []; - const sortedTypes = [...this.statTypes].sort((a, _b) => { - if (a === "min") { - return -1; - } - if (a === "max") { - return +1; - } - return 0; - }); - const drawBands = this.statTypes.includes("mean") && statisticsHaveType(stats, "mean"); + const sortedTypes = drawBands + ? [...this.statTypes].sort((a, b) => { + if (a === "min" || b === "max") { + return -1; + } + if (a === "max" || b === "min") { + return +1; + } + return 0; + }) + : this.statTypes; + sortedTypes.forEach((type) => { if (statisticsHaveType(stats, type)) { + const band = drawBands && (type === "min" || type === "max"); statTypes.push(type); - addDataSet( - `${name} (${this.hass.localize( + statDataSets.push({ + label: `${name} (${this.hass.localize( `ui.components.statistics_charts.statistic_types.${type}` - )})`, - drawBands && (type === "min" || type === "max") - ? color + "7F" - : color, - color + "7F", - false, - drawBands + )}) + `, + fill: drawBands ? type === "min" ? "+1" : type === "max" ? "-1" : false - : false - ); + : false, + borderColor: band ? color + "7F" : color, + backgroundColor: band ? color + "3F" : color + "7F", + pointRadius: 0, + data: [], + // @ts-ignore + unit: meta?.unit_of_measurement, + band, + }); } }); let prevDate: Date | null = null; // Process chart data. + let initVal: number | null = null; + let prevSum: number | null = null; stats.forEach((stat) => { const date = new Date(stat.start); if (prevDate === date) { @@ -305,7 +341,12 @@ class StatisticsChart extends LitElement { statTypes.forEach((type) => { let val: number | null; if (type === "sum") { - val = stat.state; + if (!initVal) { + initVal = val = stat.state; + prevSum = stat.sum; + } else { + val = initVal + ((stat.sum || 0) - prevSum!); + } } else { val = stat[type]; } @@ -321,6 +362,19 @@ class StatisticsChart extends LitElement { Array.prototype.push.apply(totalDataSets, statDataSets); }); + if (unit !== null) { + this._chartOptions = { + ...this._chartOptions, + scales: { + ...this._chartOptions!.scales, + y: { + ...(this._chartOptions!.scales!.y as Record), + title: { display: unit, text: unit }, + }, + }, + }; + } + this._chartData = { datasets: totalDataSets, }; diff --git a/src/components/entity/ha-statistics-picker.ts b/src/components/entity/ha-statistics-picker.ts index 22a6593ffd..46e10060d0 100644 --- a/src/components/entity/ha-statistics-picker.ts +++ b/src/components/entity/ha-statistics-picker.ts @@ -27,9 +27,8 @@ class HaStatisticsPicker extends LitElement { return html``; } - const currentStatistics = this._currentStatistics; return html` - ${currentStatistics.map( + ${this._currentStatistics.map( (statisticId) => html`
({ @@ -103,14 +115,21 @@ export const getEnergyPreferences = (hass: HomeAssistant) => type: "energy/get_prefs", }); -export const saveEnergyPreferences = ( +export const saveEnergyPreferences = async ( hass: HomeAssistant, prefs: Partial -) => - hass.callWS({ +) => { + const newPrefs = hass.callWS({ type: "energy/save_prefs", ...prefs, }); + const energyCollection = getEnergyDataCollection(hass); + energyCollection.clearPrefs(); + if (energyCollection._active) { + energyCollection.refresh(); + } + return newPrefs; +}; interface EnergySourceByType { grid?: GridSourceTypeEnergyPreference[]; @@ -128,3 +147,197 @@ export const energySourcesByType = (prefs: EnergyPreferences) => { } return types; }; + +export interface EnergyData { + start: Date; + end?: Date; + prefs: EnergyPreferences; + info: EnergyInfo; + stats: Statistics; + co2SignalConfigEntry?: ConfigEntry; + co2SignalEntity?: string; +} + +const getEnergyData = async ( + hass: HomeAssistant, + prefs: EnergyPreferences, + start: Date, + end?: Date +): Promise => { + const [configEntries, entityRegistryEntries, info] = await Promise.all([ + getConfigEntries(hass), + subscribeOne(hass.connection, subscribeEntityRegistry), + getEnergyInfo(hass), + ]); + + const co2SignalConfigEntry = configEntries.find( + (entry) => entry.domain === "co2signal" + ); + + let co2SignalEntity: string | undefined; + + if (co2SignalConfigEntry) { + for (const entry of entityRegistryEntries) { + if (entry.config_entry_id !== co2SignalConfigEntry.entry_id) { + continue; + } + + // The integration offers 2 entities. We want the % one. + const co2State = hass.states[entry.entity_id]; + if (!co2State || co2State.attributes.unit_of_measurement !== "%") { + continue; + } + + co2SignalEntity = co2State.entity_id; + break; + } + } + + const statIDs: string[] = []; + + if (co2SignalEntity !== undefined) { + statIDs.push(co2SignalEntity); + } + + for (const source of prefs.energy_sources) { + if (source.type === "solar") { + statIDs.push(source.stat_energy_from); + continue; + } + + // grid source + for (const flowFrom of source.flow_from) { + statIDs.push(flowFrom.stat_energy_from); + } + for (const flowTo of source.flow_to) { + statIDs.push(flowTo.stat_energy_to); + } + } + + const stats = await fetchStatistics(hass!, addHours(start, -1), end, statIDs); // Subtract 1 hour from start to get starting point data + + return { + start, + end, + info, + prefs, + stats, + co2SignalConfigEntry, + co2SignalEntity, + }; +}; + +export interface EnergyCollection extends Collection { + start: Date; + end?: Date; + prefs?: EnergyPreferences; + clearPrefs(): void; + setPeriod(newStart: Date, newEnd?: Date): void; + _refreshTimeout?: number; + _updatePeriodTimeout?: number; + _active: number; +} + +export const getEnergyDataCollection = ( + hass: HomeAssistant, + prefs?: EnergyPreferences +): EnergyCollection => { + if ((hass.connection as any)._energy) { + return (hass.connection as any)._energy; + } + + const collection = getCollection( + hass.connection, + "_energy", + async () => { + if (!collection.prefs) { + // This will raise if not found. + // Detect by checking `e.code === "not_found" + collection.prefs = await getEnergyPreferences(hass); + } + + 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); + + collection._refreshTimeout = window.setTimeout( + () => collection.refresh(), + nextFetch.getTime() - Date.now() + ); + } + + return getEnergyData( + hass, + collection.prefs, + collection.start, + collection.end + ); + } + ) as EnergyCollection; + + const origSubscribe = collection.subscribe; + + collection.subscribe = (subscriber: (data: EnergyData) => void) => { + const unsub = origSubscribe(subscriber); + collection._active++; + return () => { + collection._active--; + if (collection._active < 1) { + clearTimeout(collection._refreshTimeout); + collection._refreshTimeout = undefined; + } + unsub(); + }; + }; + + collection._active = 0; + collection.prefs = prefs; + const now = new Date(); + // Set start to start of today if we have data for today, otherwise yesterday + collection.start = now.getHours() > 0 ? startOfToday() : startOfYesterday(); + collection.end = now.getHours() > 0 ? endOfToday() : endOfYesterday(); + + const scheduleUpdatePeriod = () => { + collection._updatePeriodTimeout = window.setTimeout( + () => { + collection.start = startOfToday(); + collection.end = endOfToday(); + scheduleUpdatePeriod(); + }, + addHours(endOfToday(), 1).getTime() - Date.now() // Switch to next day an hour after the day changed + ); + }; + scheduleUpdatePeriod(); + + collection.clearPrefs = () => { + collection.prefs = undefined; + }; + 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() + ) { + scheduleUpdatePeriod(); + } + }; + return collection; +}; diff --git a/src/panels/energy/ha-panel-energy.ts b/src/panels/energy/ha-panel-energy.ts index c39908b83d..5539a24895 100644 --- a/src/panels/energy/ha-panel-energy.ts +++ b/src/panels/energy/ha-panel-energy.ts @@ -14,6 +14,7 @@ import { customElement, property, state } from "lit/decorators"; import "../../components/ha-menu-button"; import "../../layouts/ha-app-layout"; import { mdiCog } from "@mdi/js"; + import { haStyle } from "../../resources/styles"; import "../lovelace/views/hui-view"; import { HomeAssistant } from "../../types"; diff --git a/src/panels/energy/strategies/energy-strategy.ts b/src/panels/energy/strategies/energy-strategy.ts index 86ff55d8d1..ff033d1b25 100644 --- a/src/panels/energy/strategies/energy-strategy.ts +++ b/src/panels/energy/strategies/energy-strategy.ts @@ -1,5 +1,6 @@ import { EnergyPreferences, + getEnergyDataCollection, getEnergyPreferences, GridSourceTypeEnergyPreference, } from "../../../data/energy"; @@ -26,10 +27,10 @@ export class EnergyStrategy { const view: LovelaceViewConfig = { cards: [] }; - let energyPrefs: EnergyPreferences; + let prefs: EnergyPreferences; try { - energyPrefs = await getEnergyPreferences(hass); + prefs = await getEnergyPreferences(hass); } catch (e) { if (e.code === "not_found") { return setupWizard(); @@ -43,20 +44,25 @@ export class EnergyStrategy { view.type = "sidebar"; - const hasGrid = energyPrefs.energy_sources.find( + const hasGrid = prefs.energy_sources.find( (source) => source.type === "grid" ) as GridSourceTypeEnergyPreference; const hasReturn = hasGrid && hasGrid.flow_to.length; - const hasSolar = energyPrefs.energy_sources.some( + const hasSolar = prefs.energy_sources.some( (source) => source.type === "solar" ); + getEnergyDataCollection(hass, prefs); + + view.cards!.push({ + type: "energy-date-selection", + }); + // Only include if we have a grid source. if (hasGrid) { view.cards!.push({ title: "Energy usage", type: "energy-usage-graph", - prefs: energyPrefs, }); } @@ -65,7 +71,6 @@ export class EnergyStrategy { view.cards!.push({ title: "Solar production", type: "energy-solar-graph", - prefs: energyPrefs, }); } @@ -74,7 +79,6 @@ export class EnergyStrategy { view.cards!.push({ title: "Energy distribution", type: "energy-distribution", - prefs: energyPrefs, view_layout: { position: "sidebar" }, }); } @@ -83,16 +87,6 @@ export class EnergyStrategy { view.cards!.push({ title: "Sources", type: "energy-sources-table", - prefs: energyPrefs, - }); - } - - // Only include if we have a solar source. - if (hasSolar) { - view.cards!.push({ - type: "energy-solar-consumed-gauge", - prefs: energyPrefs, - view_layout: { position: "sidebar" }, }); } @@ -100,7 +94,14 @@ export class EnergyStrategy { if (hasReturn) { view.cards!.push({ type: "energy-grid-neutrality-gauge", - prefs: energyPrefs, + view_layout: { position: "sidebar" }, + }); + } + + // Only include if we have a solar source. + if (hasSolar && hasReturn) { + view.cards!.push({ + type: "energy-solar-consumed-gauge", view_layout: { position: "sidebar" }, }); } @@ -109,17 +110,15 @@ export class EnergyStrategy { if (hasGrid) { view.cards!.push({ type: "energy-carbon-consumed-gauge", - prefs: energyPrefs, view_layout: { position: "sidebar" }, }); } // Only include if we have at least 1 device in the config. - if (energyPrefs.device_consumption.length) { + if (prefs.device_consumption.length) { view.cards!.push({ title: "Monitor individual devices", type: "energy-devices-graph", - prefs: energyPrefs, }); } diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts index cef5aa53c3..4da57cff25 100644 --- a/src/panels/history/ha-panel-history.ts +++ b/src/panels/history/ha-panel-history.ts @@ -2,6 +2,15 @@ import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import { css, html, LitElement, PropertyValues } from "lit"; import { property, state } from "lit/decorators"; +import { + startOfWeek, + endOfWeek, + startOfToday, + endOfToday, + startOfYesterday, + endOfYesterday, + addDays, +} from "date-fns"; import { computeRTL } from "../../common/util/compute_rtl"; import "../../components/entity/ha-entity-picker"; import "../../components/ha-circular-progress"; @@ -37,15 +46,11 @@ class HaPanelHistory extends LitElement { super(); const start = new Date(); - start.setHours(start.getHours() - 2); - start.setMinutes(0); - start.setSeconds(0); + start.setHours(start.getHours() - 2, 0, 0, 0); this._startDate = start; const end = new Date(); - end.setHours(end.getHours() + 1); - end.setMinutes(0); - end.setSeconds(0); + end.setHours(end.getHours() + 1, 0, 0, 0); this._endDate = end; } @@ -108,42 +113,20 @@ class HaPanelHistory extends LitElement { super.firstUpdated(changedProps); const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayEnd = new Date(today); - todayEnd.setDate(todayEnd.getDate() + 1); - todayEnd.setMilliseconds(todayEnd.getMilliseconds() - 1); - - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); - const yesterdayEnd = new Date(today); - yesterdayEnd.setMilliseconds(yesterdayEnd.getMilliseconds() - 1); - - const thisWeekStart = new Date(today); - thisWeekStart.setDate(today.getDate() - today.getDay()); - const thisWeekEnd = new Date(thisWeekStart); - thisWeekEnd.setDate(thisWeekStart.getDate() + 7); - thisWeekEnd.setMilliseconds(thisWeekEnd.getMilliseconds() - 1); - - const lastWeekStart = new Date(today); - lastWeekStart.setDate(today.getDate() - today.getDay() - 7); - const lastWeekEnd = new Date(lastWeekStart); - lastWeekEnd.setDate(lastWeekStart.getDate() + 7); - lastWeekEnd.setMilliseconds(lastWeekEnd.getMilliseconds() - 1); + const weekStart = startOfWeek(today); + const weekEnd = endOfWeek(today); this._ranges = { - [this.hass.localize("ui.panel.history.ranges.today")]: [today, todayEnd], - [this.hass.localize("ui.panel.history.ranges.yesterday")]: [ - yesterday, - yesterdayEnd, - ], - [this.hass.localize("ui.panel.history.ranges.this_week")]: [ - thisWeekStart, - thisWeekEnd, - ], - [this.hass.localize("ui.panel.history.ranges.last_week")]: [ - lastWeekStart, - lastWeekEnd, + [this.hass.localize("ui.components.date-range-picker.ranges.today")]: [ + startOfToday(), + endOfToday(), ], + [this.hass.localize("ui.components.date-range-picker.ranges.yesterday")]: + [startOfYesterday(), endOfYesterday()], + [this.hass.localize("ui.components.date-range-picker.ranges.this_week")]: + [weekStart, weekEnd], + [this.hass.localize("ui.components.date-range-picker.ranges.last_week")]: + [addDays(weekStart, -7), addDays(weekEnd, -7)], }; } diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index 950a1a47b8..ec2d457336 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -4,6 +4,15 @@ import "@polymer/app-layout/app-header/app-header"; import "@polymer/app-layout/app-toolbar/app-toolbar"; import { css, html, LitElement, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { + addDays, + endOfToday, + endOfWeek, + endOfYesterday, + startOfToday, + startOfWeek, + startOfYesterday, +} from "date-fns"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeRTL } from "../../common/util/compute_rtl"; @@ -55,17 +64,11 @@ export class HaPanelLogbook extends LitElement { super(); const start = new Date(); - start.setHours(start.getHours() - 2); - start.setMinutes(0); - start.setSeconds(0); - start.setMilliseconds(0); + start.setHours(start.getHours() - 2, 0, 0, 0); this._startDate = start; const end = new Date(); - end.setHours(end.getHours() + 1); - end.setMinutes(0); - end.setSeconds(0); - end.setMilliseconds(0); + end.setHours(end.getHours() + 1, 0, 0, 0); this._endDate = end; } @@ -140,42 +143,20 @@ export class HaPanelLogbook extends LitElement { this._fetchUserPromise = this._fetchUserNames(); const today = new Date(); - today.setHours(0, 0, 0, 0); - const todayEnd = new Date(today); - todayEnd.setDate(todayEnd.getDate() + 1); - todayEnd.setMilliseconds(todayEnd.getMilliseconds() - 1); - - const yesterday = new Date(today); - yesterday.setDate(today.getDate() - 1); - const yesterdayEnd = new Date(today); - yesterdayEnd.setMilliseconds(yesterdayEnd.getMilliseconds() - 1); - - const thisWeekStart = new Date(today); - thisWeekStart.setDate(today.getDate() - today.getDay()); - const thisWeekEnd = new Date(thisWeekStart); - thisWeekEnd.setDate(thisWeekStart.getDate() + 7); - thisWeekEnd.setMilliseconds(thisWeekEnd.getMilliseconds() - 1); - - const lastWeekStart = new Date(today); - lastWeekStart.setDate(today.getDate() - today.getDay() - 7); - const lastWeekEnd = new Date(lastWeekStart); - lastWeekEnd.setDate(lastWeekStart.getDate() + 7); - lastWeekEnd.setMilliseconds(lastWeekEnd.getMilliseconds() - 1); + const weekStart = startOfWeek(today); + const weekEnd = endOfWeek(today); this._ranges = { - [this.hass.localize("ui.panel.logbook.ranges.today")]: [today, todayEnd], - [this.hass.localize("ui.panel.logbook.ranges.yesterday")]: [ - yesterday, - yesterdayEnd, - ], - [this.hass.localize("ui.panel.logbook.ranges.this_week")]: [ - thisWeekStart, - thisWeekEnd, - ], - [this.hass.localize("ui.panel.logbook.ranges.last_week")]: [ - lastWeekStart, - lastWeekEnd, + [this.hass.localize("ui.components.date-range-picker.ranges.today")]: [ + startOfToday(), + endOfToday(), ], + [this.hass.localize("ui.components.date-range-picker.ranges.yesterday")]: + [startOfYesterday(), endOfYesterday()], + [this.hass.localize("ui.components.date-range-picker.ranges.this_week")]: + [weekStart, weekEnd], + [this.hass.localize("ui.components.date-range-picker.ranges.last_week")]: + [addDays(weekStart, -7), addDays(weekEnd, -7)], }; } diff --git a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts index 84a2e54206..3812e75059 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-carbon-consumed-gauge-card.ts @@ -1,19 +1,20 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import { round } from "../../../../common/number/round"; -import { subscribeOne } from "../../../../common/util/subscribe-one"; import "../../../../components/ha-card"; import "../../../../components/ha-gauge"; -import { getConfigEntries } from "../../../../data/config_entries"; -import { energySourcesByType } from "../../../../data/energy"; -import { subscribeEntityRegistry } from "../../../../data/entity_registry"; +import { + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; import { calculateStatisticsSumGrowth, calculateStatisticsSumGrowthWithPercentage, - fetchStatistics, - Statistics, } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import { createEntityNotFoundWarning } from "../../components/hui-warning"; import type { LovelaceCard } from "../../types"; @@ -21,14 +22,15 @@ import { severityMap } from "../hui-gauge-card"; import type { EnergyCarbonGaugeCardConfig } from "../types"; @customElement("hui-energy-carbon-consumed-gauge-card") -class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { +class HuiEnergyCarbonGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergyCarbonGaugeCardConfig; - @state() private _stats?: Statistics; - - @state() private _co2SignalEntity?: string | null; + @state() private _data?: EnergyData; public getCardSize(): number { return 4; @@ -38,12 +40,12 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { this._config = config; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this.hasUpdated) { - this._getStatistics(); - } + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => { + this._data = data; + }), + ]; } protected render(): TemplateResult { @@ -51,58 +53,66 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { return html``; } - if (this._co2SignalEntity === null) { - return html``; - } - - if (!this._stats || !this._co2SignalEntity) { + if (!this._data) { return html`Loading...`; } - const co2State = this.hass.states[this._co2SignalEntity]; + if (!this._data.co2SignalEntity) { + return html``; + } + + const co2State = this.hass.states[this._data.co2SignalEntity]; if (!co2State) { return html` - ${createEntityNotFoundWarning(this.hass, this._co2SignalEntity)} + ${createEntityNotFoundWarning(this.hass, this._data.co2SignalEntity)} `; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const types = energySourcesByType(prefs); const totalGridConsumption = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_from.map((flow) => flow.stat_energy_from) ); let value: number | undefined; - if (this._co2SignalEntity in this._stats && totalGridConsumption) { + if (totalGridConsumption === 0) { + value = 100; + } + + if ( + this._data.co2SignalEntity in this._data.stats && + totalGridConsumption + ) { const highCarbonEnergy = calculateStatisticsSumGrowthWithPercentage( - this._stats[this._co2SignalEntity], + this._data.stats[this._data.co2SignalEntity], types .grid![0].flow_from.map( - (flow) => this._stats![flow.stat_energy_from] + (flow) => this._data!.stats![flow.stat_energy_from] ) .filter(Boolean) ) || 0; const totalSolarProduction = types.solar ? calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.solar.map((source) => source.stat_energy_from) - ) - : undefined; + ) || 0 + : 0; - const totalGridReturned = calculateStatisticsSumGrowth( - this._stats, - types.grid![0].flow_to.map((flow) => flow.stat_energy_to) - ); + const totalGridReturned = + calculateStatisticsSumGrowth( + this._data.stats, + types.grid![0].flow_to.map((flow) => flow.stat_energy_to) + ) || 0; const totalEnergyConsumed = totalGridConsumption + - Math.max(0, (totalSolarProduction || 0) - (totalGridReturned || 0)); + Math.max(0, totalSolarProduction - totalGridReturned); value = round((1 - highCarbonEnergy / totalEnergyConsumed) * 100); } @@ -139,78 +149,6 @@ class HuiEnergyCarbonGaugeCard extends LitElement implements LovelaceCard { return severityMap.normal; } - private async _fetchCO2SignalEntity() { - const [configEntries, entityRegistryEntries] = await Promise.all([ - getConfigEntries(this.hass), - subscribeOne(this.hass.connection, subscribeEntityRegistry), - ]); - - const co2ConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); - - if (!co2ConfigEntry) { - this._co2SignalEntity = null; - return; - } - - for (const entry of entityRegistryEntries) { - if (entry.config_entry_id !== co2ConfigEntry.entry_id) { - continue; - } - - // The integration offers 2 entities. We want the % one. - const co2State = this.hass.states[entry.entity_id]; - if (!co2State || co2State.attributes.unit_of_measurement !== "%") { - continue; - } - - this._co2SignalEntity = co2State.entity_id; - return; - } - this._co2SignalEntity = null; - } - - private async _getStatistics(): Promise { - await this._fetchCO2SignalEntity(); - - if (this._co2SignalEntity === null) { - return; - } - - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - if (this._co2SignalEntity) { - statistics.push(this._co2SignalEntity); - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ha-card { diff --git a/src/panels/lovelace/cards/energy/hui-energy-date-selection-card.ts b/src/panels/lovelace/cards/energy/hui-energy-date-selection-card.ts new file mode 100644 index 0000000000..bde7487152 --- /dev/null +++ b/src/panels/lovelace/cards/energy/hui-energy-date-selection-card.ts @@ -0,0 +1,120 @@ +import { + startOfWeek, + endOfWeek, + startOfToday, + endOfToday, + startOfYesterday, + endOfYesterday, + addDays, +} 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"; +import "../../../../components/chart/ha-chart-base"; +import "../../../../components/ha-card"; +import "../../../../components/ha-date-range-picker"; +import type { DateRangePickerRanges } from "../../../../components/ha-date-range-picker"; +import { EnergyData, getEnergyDataCollection } from "../../../../data/energy"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import { HomeAssistant } from "../../../../types"; +import { LovelaceCard } from "../../types"; +import { EnergyDevicesGraphCardConfig } from "../types"; + +@customElement("hui-energy-date-selection-card") +export class HuiEnergyDateSelectionCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _config?: EnergyDevicesGraphCardConfig; + + @state() private _ranges?: DateRangePickerRanges; + + @state() _startDate?: Date; + + @state() _endDate?: Date; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => + this._updateDates(data) + ), + ]; + } + + public willUpdate() { + if (!this.hasUpdated) { + const today = new Date(); + const weekStart = startOfWeek(today); + const weekEnd = endOfWeek(today); + + this._ranges = { + [this.hass.localize("ui.components.date-range-picker.ranges.today")]: [ + startOfToday(), + endOfToday(), + ], + [this.hass.localize( + "ui.components.date-range-picker.ranges.yesterday" + )]: [startOfYesterday(), endOfYesterday()], + [this.hass.localize( + "ui.components.date-range-picker.ranges.this_week" + )]: [weekStart, weekEnd], + [this.hass.localize( + "ui.components.date-range-picker.ranges.last_week" + )]: [addDays(weekStart, -7), addDays(weekEnd, -7)], + }; + } + } + + public getCardSize(): Promise | number { + return 1; + } + + public setConfig(config: EnergyDevicesGraphCardConfig): void { + this._config = config; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config || !this._startDate) { + return html``; + } + + return html` + + `; + } + + private _updateDates(energyData: EnergyData): void { + this._startDate = energyData.start; + this._endDate = energyData.end || endOfToday(); + } + + private _dateRangeChanged(ev: CustomEvent): void { + if ( + ev.detail.startDate.getTime() === this._startDate!.getTime() && + ev.detail.endDate.getTime() === this._endDate!.getTime() + ) { + return; + } + const energyCollection = getEnergyDataCollection(this.hass); + energyCollection.setPeriod(ev.detail.startDate, ev.detail.endDate); + energyCollection.refresh(); + } + + static get styles(): CSSResultGroup { + return css``; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-energy-date-selection-card": HuiEnergyDateSelectionCard; + } +} 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 f9d8c1cb5d..61688949c9 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,16 +4,12 @@ import { ChartOptions, ParsedDataType, } from "chart.js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { addHours } 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"; import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; import { getColorByIndex } from "../../../../common/color/colors"; import { computeStateName } from "../../../../common/entity/compute_state_name"; import { @@ -22,18 +18,21 @@ import { } from "../../../../common/string/format_number"; import "../../../../components/chart/ha-chart-base"; import "../../../../components/ha-card"; +import { EnergyData, getEnergyDataCollection } from "../../../../data/energy"; import { calculateStatisticSumGrowth, fetchStatistics, Statistics, } from "../../../../data/history"; +import { FrontendLocaleData } from "../../../../data/translation"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergyDevicesGraphCardConfig } from "../types"; @customElement("hui-energy-devices-graph-card") export class HuiEnergyDevicesGraphCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @@ -42,34 +41,14 @@ export class HuiEnergyDevicesGraphCard @state() private _data?: Statistics; - @state() private _chartData?: ChartData; + @state() private _chartData: ChartData = { datasets: [] }; - @state() private _chartOptions?: ChartOptions; - - private _fetching = false; - - private _interval?: number; - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._interval) { - clearInterval(this._interval); - this._interval = undefined; - } - } - - public connectedCallback() { - super.connectedCallback(); - if (!this.hasUpdated) { - return; - } - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => + this._getStatistics(data) + ), + ]; } public getCardSize(): Promise | number { @@ -80,30 +59,6 @@ export class HuiEnergyDevicesGraphCard this._config = config; } - public willUpdate(changedProps: PropertyValues) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this._createOptions(); - } - if (!this._config || !changedProps.has("_config")) { - return; - } - - const oldConfig = changedProps.get("_config") as - | EnergyDevicesGraphCardConfig - | undefined; - - if (oldConfig !== this._config) { - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -119,25 +74,30 @@ export class HuiEnergyDevicesGraphCard "has-header": !!this._config.title, })}" > - ${this._chartData - ? html`` - : ""} +
`; } - private _createOptions() { - this._chartOptions = { + private _createOptions = memoizeOne( + (locale: FrontendLocaleData): ChartOptions => ({ parsing: false, animation: false, responsive: true, + maintainAspectRatio: false, indexAxis: "y", scales: { + y: { + type: "category", + ticks: { autoSkip: false }, + }, x: { title: { display: true, @@ -153,37 +113,25 @@ export class HuiEnergyDevicesGraphCard label: (context) => `${context.dataset.label}: ${formatNumber( context.parsed.x, - this.hass.locale + locale )} kWh`, }, }, }, // @ts-expect-error locale: numberFormatToLocale(this.hass.locale), - }; - } + }) + ); - private async _getStatistics(): Promise { - if (this._fetching) { - return; - } - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - this._fetching = true; - const prefs = this._config!.prefs; - - try { - this._data = await fetchStatistics( - this.hass!, - startDate, - undefined, - prefs.device_consumption.map((device) => device.stat_consumption) - ); - } finally { - this._fetching = false; - } + private async _getStatistics(energyData: EnergyData): Promise { + this._data = await fetchStatistics( + this.hass, + addHours(energyData.start, -1), + energyData.end, + energyData.prefs.device_consumption.map( + (device) => device.stat_consumption + ) + ); const statisticsData = Object.values(this._data!); let endTime: Date; @@ -210,11 +158,12 @@ export class HuiEnergyDevicesGraphCard borderColor, backgroundColor, data, + barThickness: 20, }, ]; - for (let idx = 0; idx < prefs.device_consumption.length; idx++) { - const device = prefs.device_consumption[idx]; + for (let idx = 0; idx < energyData.prefs.device_consumption.length; idx++) { + const device = energyData.prefs.device_consumption[idx]; const entity = this.hass.states[device.stat_consumption]; const label = entity ? computeStateName(entity) : device.stat_consumption; @@ -225,12 +174,14 @@ export class HuiEnergyDevicesGraphCard const value = device.stat_consumption in this._data - ? calculateStatisticSumGrowth(this._data[device.stat_consumption]) + ? calculateStatisticSumGrowth(this._data[device.stat_consumption]) || + 0 : 0; + data.push({ // @ts-expect-error y: label, - x: value || 0, + x: value, }); } @@ -243,9 +194,6 @@ export class HuiEnergyDevicesGraphCard static get styles(): CSSResultGroup { return css` - ha-card { - height: 100%; - } .card-header { padding-bottom: 0; } @@ -255,6 +203,9 @@ export class HuiEnergyDevicesGraphCard .has-header { padding-top: 0; } + ha-chart-base { + --chart-max-height: none; + } `; } } 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 a3b27745f6..2f5ac887eb 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-distribution-card.ts @@ -6,23 +6,24 @@ import { mdiSolarPower, mdiTransmissionTower, } from "@mdi/js"; +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 { formatNumber } from "../../../../common/string/format_number"; -import { subscribeOne } from "../../../../common/util/subscribe-one"; import "../../../../components/ha-card"; import "../../../../components/ha-svg-icon"; -import { getConfigEntries } from "../../../../data/config_entries"; -import { energySourcesByType } from "../../../../data/energy"; -import { subscribeEntityRegistry } from "../../../../data/entity_registry"; +import { + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; import { calculateStatisticsSumGrowth, calculateStatisticsSumGrowthWithPercentage, - fetchStatistics, - Statistics, } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergyDistributionCardConfig } from "../types"; @@ -30,34 +31,30 @@ import { EnergyDistributionCardConfig } from "../types"; const CIRCLE_CIRCUMFERENCE = 238.76104; @customElement("hui-energy-distribution-card") -class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { +class HuiEnergyDistrubutionCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergyDistributionCardConfig; - @state() private _stats?: Statistics; - - @state() private _co2SignalEntity?: string; - - private _fetching = false; + @state() private _data?: EnergyData; public setConfig(config: EnergyDistributionCardConfig): void { this._config = config; } - public getCardSize(): Promise | number { - return 3; + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => { + this._data = data; + }), + ]; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this._fetching && !this._stats) { - this._fetching = true; - this._getStatistics().then(() => { - this._fetching = false; - }); - } + public getCardSize(): Promise | number { + return 3; } protected render() { @@ -65,11 +62,11 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading…`; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const types = energySourcesByType(prefs); // The strategy only includes this card if we have a grid. @@ -80,7 +77,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { const totalGridConsumption = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_from.map((flow) => flow.stat_energy_from) ) ?? 0; @@ -89,7 +86,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { if (hasSolarProduction) { totalSolarProduction = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.solar!.map((source) => source.stat_energy_from) ) || 0; } @@ -99,7 +96,7 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { if (hasReturnToGrid) { productionReturnedToGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_to.map((flow) => flow.stat_energy_to) ) || 0; } @@ -124,16 +121,21 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { let electricityMapUrl: string | undefined; - if (this._co2SignalEntity && this._co2SignalEntity in this._stats) { + if ( + this._data.co2SignalEntity && + this._data.co2SignalEntity in this._data.stats + ) { // Calculate high carbon consumption const highCarbonConsumption = calculateStatisticsSumGrowthWithPercentage( - this._stats[this._co2SignalEntity], + this._data.stats[this._data.co2SignalEntity], types - .grid![0].flow_from.map((flow) => this._stats![flow.stat_energy_from]) + .grid![0].flow_from.map( + (flow) => this._data!.stats[flow.stat_energy_from] + ) .filter(Boolean) ); - const co2State = this.hass.states[this._co2SignalEntity]; + const co2State = this.hass.states[this._data.co2SignalEntity]; if (co2State) { electricityMapUrl = `https://www.electricitymap.org/zone/${co2State.attributes.country_code}`; @@ -401,69 +403,6 @@ class HuiEnergyDistrubutionCard extends LitElement implements LovelaceCard { `; } - private async _getStatistics(): Promise { - const [configEntries, entityRegistryEntries] = await Promise.all([ - getConfigEntries(this.hass), - subscribeOne(this.hass.connection, subscribeEntityRegistry), - ]); - - const co2ConfigEntry = configEntries.find( - (entry) => entry.domain === "co2signal" - ); - - this._co2SignalEntity = undefined; - - if (co2ConfigEntry) { - for (const entry of entityRegistryEntries) { - if (entry.config_entry_id !== co2ConfigEntry.entry_id) { - continue; - } - - // The integration offers 2 entities. We want the % one. - const co2State = this.hass.states[entry.entity_id]; - if (!co2State || co2State.attributes.unit_of_measurement !== "%") { - continue; - } - - this._co2SignalEntity = co2State.entity_id; - break; - } - } - - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - - if (this._co2SignalEntity !== undefined) { - statistics.push(this._co2SignalEntity); - } - - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static styles = css` :host { --mdc-icon-size: 24px; 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 97f8dab168..34ccbd0e98 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 @@ -1,15 +1,17 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { formatNumber } from "../../../../common/string/format_number"; import "../../../../components/ha-card"; import "../../../../components/ha-gauge"; import type { LevelDefinition } from "../../../../components/ha-gauge"; -import { GridSourceTypeEnergyPreference } from "../../../../data/energy"; import { - calculateStatisticsSumGrowth, - fetchStatistics, - Statistics, -} from "../../../../data/history"; + EnergyData, + getEnergyDataCollection, + GridSourceTypeEnergyPreference, +} from "../../../../data/energy"; +import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; import type { EnergyGridGaugeCardConfig } from "../types"; @@ -21,12 +23,23 @@ const LEVELS: LevelDefinition[] = [ ]; @customElement("hui-energy-grid-neutrality-gauge-card") -class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { +class HuiEnergyGridGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: EnergyGridGaugeCardConfig; - @state() private _stats?: Statistics; + @state() private _data?: EnergyData; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass!).subscribe((data) => { + this._data = data; + }), + ]; + } public getCardSize(): number { return 4; @@ -36,24 +49,16 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { this._config = config; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this.hasUpdated) { - this._getStatistics(); - } - } - protected render(): TemplateResult { if (!this._config || !this.hass) { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading...`; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const gridSource = prefs.energy_sources.find( (src) => src.type === "grid" ) as GridSourceTypeEnergyPreference | undefined; @@ -65,12 +70,12 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { } const consumedFromGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, gridSource.flow_from.map((flow) => flow.stat_energy_from) ); const returnedToGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, gridSource.flow_to.map((flow) => flow.stat_energy_to) ); @@ -111,35 +116,6 @@ class HuiEnergyGridGaugeCard extends LitElement implements LovelaceCard { `; } - private async _getStatistics(): Promise { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ha-card { 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 3d79fe48ab..0702fb9805 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 @@ -1,26 +1,39 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import "../../../../components/ha-card"; import "../../../../components/ha-gauge"; -import { energySourcesByType } from "../../../../data/energy"; import { - calculateStatisticsSumGrowth, - fetchStatistics, - Statistics, -} from "../../../../data/history"; + EnergyData, + energySourcesByType, + getEnergyDataCollection, +} from "../../../../data/energy"; +import { calculateStatisticsSumGrowth } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import type { HomeAssistant } from "../../../../types"; import type { LovelaceCard } from "../../types"; import { severityMap } from "../hui-gauge-card"; import type { EnergySolarGaugeCardConfig } from "../types"; @customElement("hui-energy-solar-consumed-gauge-card") -class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { +class HuiEnergySolarGaugeCard + extends SubscribeMixin(LitElement) + implements LovelaceCard +{ @property({ attribute: false }) public hass?: HomeAssistant; @state() private _config?: EnergySolarGaugeCardConfig; - @state() private _stats?: Statistics; + @state() private _data?: EnergyData; + + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass!).subscribe((data) => { + this._data = data; + }), + ]; + } public getCardSize(): number { return 4; @@ -30,33 +43,29 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { this._config = config; } - public willUpdate(changedProps) { - super.willUpdate(changedProps); - - if (!this.hasUpdated) { - this._getStatistics(); - } - } - protected render(): TemplateResult { if (!this._config || !this.hass) { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading...`; } - const prefs = this._config!.prefs; + const prefs = this._data.prefs; const types = energySourcesByType(prefs); + if (!types.solar) { + return html``; + } + const totalSolarProduction = calculateStatisticsSumGrowth( - this._stats, - types.solar!.map((source) => source.stat_energy_from) + this._data.stats, + types.solar.map((source) => source.stat_energy_from) ); const productionReturnedToGrid = calculateStatisticsSumGrowth( - this._stats, + this._data.stats, types.grid![0].flow_to.map((flow) => flow.stat_energy_to) ); @@ -101,36 +110,6 @@ class HuiEnergySolarGaugeCard extends LitElement implements LovelaceCard { return severityMap.normal; } - private async _getStatistics(): Promise { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = []; - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - continue; - } - - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ha-card { 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 391585329b..ce2a5d253f 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 @@ -1,19 +1,14 @@ -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +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 { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergySolarGraphCardConfig } from "../types"; -import { fetchStatistics, Statistics } from "../../../../data/history"; import { hex2rgb, lab2rgb, @@ -21,7 +16,11 @@ import { rgb2lab, } from "../../../../common/color/convert-color"; import { labDarken } from "../../../../common/color/lab"; -import { SolarSourceTypeEnergyPreference } from "../../../../data/energy"; +import { + EnergyData, + getEnergyDataCollection, + SolarSourceTypeEnergyPreference, +} from "../../../../data/energy"; import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; import { ForecastSolarForecast, @@ -35,52 +34,34 @@ import { formatNumber, numberFormatToLocale, } from "../../../../common/string/format_number"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; +import { FrontendLocaleData } from "../../../../data/translation"; @customElement("hui-energy-solar-graph-card") export class HuiEnergySolarGraphCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergySolarGraphCardConfig; - @state() private _data?: Statistics; - @state() private _chartData: ChartData = { datasets: [], }; @state() private _forecasts?: Record; - @state() private _chartOptions?: ChartOptions; + @state() private _start = startOfToday(); - @state() private _showAllForecastData = false; + @state() private _end = endOfToday(); - private _fetching = false; - - private _interval?: number; - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._interval) { - clearInterval(this._interval); - this._interval = undefined; - } - } - - public connectedCallback() { - super.connectedCallback(); - if (!this.hasUpdated) { - return; - } - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => + this._getStatistics(data) + ), + ]; } public getCardSize(): Promise | number { @@ -91,30 +72,6 @@ export class HuiEnergySolarGraphCard this._config = config; } - public willUpdate(changedProps: PropertyValues) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this._createOptions(); - } - if (!this._config || !changedProps.has("_config")) { - return; - } - - const oldConfig = changedProps.get("_config") as - | EnergySolarGraphCardConfig - | undefined; - - if (oldConfig !== this._config) { - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -132,7 +89,11 @@ export class HuiEnergySolarGraphCard >
@@ -140,22 +101,18 @@ export class HuiEnergySolarGraphCard `; } - private _createOptions() { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - const startTime = startDate.getTime(); - - this._chartOptions = { + private _createOptions = memoizeOne( + (start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => ({ parsing: false, animation: false, scales: { x: { type: "time", - suggestedMin: startTime, - suggestedMax: startTime + 24 * 60 * 60 * 1000, + suggestedMin: start.getTime(), + suggestedMax: end.getTime(), adapters: { date: { - locale: this.hass.locale, + locale: locale, }, }, ticks: { @@ -193,7 +150,7 @@ export class HuiEnergySolarGraphCard label: (context) => `${context.dataset.label}: ${formatNumber( context.parsed.y, - this.hass.locale + locale )} kWh`, }, }, @@ -221,37 +178,16 @@ export class HuiEnergySolarGraphCard }, }, // @ts-expect-error - locale: numberFormatToLocale(this.hass.locale), - }; - } - - private async _getStatistics(): Promise { - if (this._fetching) { - return; - } - - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - this._fetching = true; + locale: numberFormatToLocale(locale), + }) + ); + private async _getStatistics(energyData: EnergyData): Promise { const solarSources: SolarSourceTypeEnergyPreference[] = - this._config!.prefs.energy_sources.filter( + energyData.prefs.energy_sources.filter( (source) => source.type === "solar" ) as SolarSourceTypeEnergyPreference[]; - try { - this._data = await fetchStatistics( - this.hass!, - startDate, - undefined, - solarSources.map((source) => source.stat_energy_from) - ); - } finally { - this._fetching = false; - } - if ( isComponentLoaded(this.hass, "forecast_solar") && solarSources.some((source) => source.config_entry_solar_forecast) @@ -259,16 +195,7 @@ export class HuiEnergySolarGraphCard this._forecasts = await getForecastSolarForecasts(this.hass); } - this._renderChart(); - } - - private _renderChart() { - const solarSources: SolarSourceTypeEnergyPreference[] = - this._config!.prefs.energy_sources.filter( - (source) => source.type === "solar" - ) as SolarSourceTypeEnergyPreference[]; - - const statisticsData = Object.values(this._data!); + const statisticsData = Object.values(energyData.stats); const datasets: ChartDataset<"bar">[] = []; let endTime: Date; @@ -311,8 +238,8 @@ export class HuiEnergySolarGraphCard let prevStart: string | null = null; // Process solar production data. - if (this._data![source.stat_energy_from]) { - for (const point of this._data![source.stat_energy_from]) { + if (energyData.stats[source.stat_energy_from]) { + for (const point of energyData.stats[source.stat_energy_from]) { if (!point.sum) { continue; } @@ -372,14 +299,12 @@ export class HuiEnergySolarGraphCard }; data.push(forecast); - const today = new Date(); - const tomorrow = new Date(today); - tomorrow.setDate(tomorrow.getDate() + 1); - tomorrow.setHours(0, 0, 0, 0); - for (const [date, value] of Object.entries(forecastsData)) { const dateObj = new Date(date); - if (dateObj > tomorrow && !this._showAllForecastData) { + if ( + dateObj < energyData.start || + (energyData.end && dateObj > energyData.end) + ) { continue; } forecast.data.push({ @@ -394,6 +319,9 @@ export class HuiEnergySolarGraphCard Array.prototype.push.apply(datasets, data); }); + this._start = energyData.start; + this._end = energyData.end || endOfToday(); + this._chartData = { datasets, }; diff --git a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts index 83252fe9fe..68c1615ac6 100644 --- a/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts +++ b/src/panels/lovelace/cards/energy/hui-energy-sources-table-card.ts @@ -1,5 +1,6 @@ // @ts-ignore import dataTableStyles from "@material/data-table/dist/mdc.data-table.min.css"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, @@ -22,31 +23,34 @@ import { formatNumber } from "../../../../common/string/format_number"; import "../../../../components/chart/statistics-chart"; import "../../../../components/ha-card"; import { - EnergyInfo, + EnergyData, energySourcesByType, - getEnergyInfo, + getEnergyDataCollection, } from "../../../../data/energy"; -import { - calculateStatisticSumGrowth, - fetchStatistics, - Statistics, -} from "../../../../data/history"; +import { calculateStatisticSumGrowth } from "../../../../data/history"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergySourcesTableCardConfig } from "../types"; @customElement("hui-energy-sources-table-card") export class HuiEnergySourcesTableCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergySourcesTableCardConfig; - @state() private _stats?: Statistics; + @state() private _data?: EnergyData; - @state() private _energyInfo?: EnergyInfo; + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => { + this._data = data; + }), + ]; + } public getCardSize(): Promise | number { return 3; @@ -56,18 +60,12 @@ export class HuiEnergySourcesTableCard this._config = config; } - public willUpdate() { - if (!this.hasUpdated) { - this._getEnergyInfo().then(() => this._getStatistics()); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; } - if (!this._stats) { + if (!this._data) { return html`Loading...`; } @@ -75,7 +73,7 @@ export class HuiEnergySourcesTableCard let totalSolar = 0; let totalCost = 0; - const types = energySourcesByType(this._config.prefs); + const types = energySourcesByType(this._data.prefs); const computedStyles = getComputedStyle(this); const solarColor = computedStyles @@ -140,7 +138,7 @@ export class HuiEnergySourcesTableCard const entity = this.hass.states[source.stat_energy_from]; const energy = calculateStatisticSumGrowth( - this._stats![source.stat_energy_from] + this._data!.stats[source.stat_energy_from] ) || 0; totalSolar += energy; const color = @@ -195,14 +193,16 @@ export class HuiEnergySourcesTableCard const entity = this.hass.states[flow.stat_energy_from]; const energy = calculateStatisticSumGrowth( - this._stats![flow.stat_energy_from] + this._data!.stats[flow.stat_energy_from] ) || 0; totalGrid += energy; const cost_stat = flow.stat_cost || - this._energyInfo!.cost_sensors[flow.stat_energy_from]; + this._data!.info.cost_sensors[flow.stat_energy_from]; const cost = cost_stat - ? calculateStatisticSumGrowth(this._stats![cost_stat]) || 0 + ? calculateStatisticSumGrowth( + this._data!.stats[cost_stat] + ) || 0 : null; if (cost !== null) { totalCost += cost; @@ -253,15 +253,16 @@ export class HuiEnergySourcesTableCard const entity = this.hass.states[flow.stat_energy_to]; const energy = (calculateStatisticSumGrowth( - this._stats![flow.stat_energy_to] + this._data!.stats[flow.stat_energy_to] ) || 0) * -1; totalGrid += energy; const cost_stat = flow.stat_compensation || - this._energyInfo!.cost_sensors[flow.stat_energy_to]; + this._data!.info.cost_sensors[flow.stat_energy_to]; const cost = cost_stat - ? (calculateStatisticSumGrowth(this._stats![cost_stat]) || - 0) * -1 + ? (calculateStatisticSumGrowth( + this._data!.stats[cost_stat] + ) || 0) * -1 : null; if (cost !== null) { totalCost += cost; @@ -333,45 +334,6 @@ export class HuiEnergySourcesTableCard `; } - private async _getEnergyInfo() { - this._energyInfo = await getEnergyInfo(this.hass); - } - - private async _getStatistics(): Promise { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - const statistics: string[] = Object.values(this._energyInfo!.cost_sensors); - const prefs = this._config!.prefs; - for (const source of prefs.energy_sources) { - if (source.type === "solar") { - statistics.push(source.stat_energy_from); - } else { - // grid source - for (const flowFrom of source.flow_from) { - statistics.push(flowFrom.stat_energy_from); - if (flowFrom.stat_cost) { - statistics.push(flowFrom.stat_cost); - } - } - for (const flowTo of source.flow_to) { - statistics.push(flowTo.stat_energy_to); - if (flowTo.stat_compensation) { - statistics.push(flowTo.stat_compensation); - } - } - } - } - - this._stats = await fetchStatistics( - this.hass!, - startDate, - undefined, - statistics - ); - } - static get styles(): CSSResultGroup { return css` ${unsafeCSS(dataTableStyles)} 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 5c455a4865..0045879545 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,14 +1,10 @@ import { ChartData, ChartDataset, ChartOptions } from "chart.js"; -import { - css, - CSSResultGroup, - html, - LitElement, - PropertyValues, - TemplateResult, -} from "lit"; +import { startOfToday, endOfToday } 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"; import { classMap } from "lit/directives/class-map"; +import memoizeOne from "memoize-one"; import { hex2rgb, lab2rgb, @@ -24,52 +20,36 @@ import { } from "../../../../common/string/format_number"; import "../../../../components/chart/ha-chart-base"; import "../../../../components/ha-card"; -import { fetchStatistics, Statistics } from "../../../../data/history"; +import { EnergyData, getEnergyDataCollection } from "../../../../data/energy"; +import { FrontendLocaleData } from "../../../../data/translation"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { HomeAssistant } from "../../../../types"; import { LovelaceCard } from "../../types"; import { EnergyUsageGraphCardConfig } from "../types"; @customElement("hui-energy-usage-graph-card") export class HuiEnergyUsageGraphCard - extends LitElement + extends SubscribeMixin(LitElement) implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @state() private _config?: EnergyUsageGraphCardConfig; - @state() private _data?: Statistics; - @state() private _chartData: ChartData = { datasets: [], }; - @state() private _chartOptions?: ChartOptions; + @state() private _start = startOfToday(); - private _fetching = false; + @state() private _end = endOfToday(); - private _interval?: number; - - public disconnectedCallback() { - super.disconnectedCallback(); - if (this._interval) { - clearInterval(this._interval); - this._interval = undefined; - } - } - - public connectedCallback() { - super.connectedCallback(); - if (!this.hasUpdated) { - return; - } - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); + public hassSubscribe(): UnsubscribeFunc[] { + return [ + getEnergyDataCollection(this.hass).subscribe((data) => + this._getStatistics(data) + ), + ]; } public getCardSize(): Promise | number { @@ -80,30 +60,6 @@ export class HuiEnergyUsageGraphCard this._config = config; } - public willUpdate(changedProps: PropertyValues) { - super.willUpdate(changedProps); - if (!this.hasUpdated) { - this._createOptions(); - } - if (!this._config || !changedProps.has("_config")) { - return; - } - - const oldConfig = changedProps.get("_config") as - | EnergyUsageGraphCardConfig - | undefined; - - if (oldConfig !== this._config) { - this._getStatistics(); - // statistics are created every hour - clearInterval(this._interval); - this._interval = window.setInterval( - () => this._getStatistics(), - 1000 * 60 * 60 - ); - } - } - protected render(): TemplateResult { if (!this.hass || !this._config) { return html``; @@ -121,7 +77,11 @@ export class HuiEnergyUsageGraphCard > @@ -129,22 +89,18 @@ export class HuiEnergyUsageGraphCard `; } - private _createOptions() { - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - const startTime = startDate.getTime(); - - this._chartOptions = { + private _createOptions = memoizeOne( + (start: Date, end: Date, locale: FrontendLocaleData): ChartOptions => ({ parsing: false, animation: false, scales: { x: { type: "time", - suggestedMin: startTime, - suggestedMax: startTime + 24 * 60 * 60 * 1000, + suggestedMin: start.getTime(), + suggestedMax: end.getTime(), adapters: { date: { - locale: this.hass.locale, + locale: locale, }, }, ticks: { @@ -173,8 +129,7 @@ export class HuiEnergyUsageGraphCard }, ticks: { beginAtZero: true, - callback: (value) => - formatNumber(Math.abs(value), this.hass.locale), + callback: (value) => formatNumber(Math.abs(value), locale), }, }, }, @@ -188,7 +143,7 @@ export class HuiEnergyUsageGraphCard label: (context) => `${context.dataset.label}: ${formatNumber( Math.abs(context.parsed.y), - this.hass.locale + locale )} kWh`, footer: (contexts) => { let totalConsumed = 0; @@ -204,16 +159,10 @@ export class HuiEnergyUsageGraphCard } return [ totalConsumed - ? `Total consumed: ${formatNumber( - totalConsumed, - this.hass.locale - )} kWh` + ? `Total consumed: ${formatNumber(totalConsumed, locale)} kWh` : "", totalReturned - ? `Total returned: ${formatNumber( - totalReturned, - this.hass.locale - )} kWh` + ? `Total returned: ${formatNumber(totalReturned, locale)} kWh` : "", ].filter(Boolean); }, @@ -239,27 +188,18 @@ export class HuiEnergyUsageGraphCard }, }, // @ts-expect-error - locale: numberFormatToLocale(this.hass.locale), - }; - } + locale: numberFormatToLocale(locale), + }) + ); - private async _getStatistics(): Promise { - if (this._fetching) { - return; - } - const startDate = new Date(); - startDate.setHours(0, 0, 0, 0); - startDate.setTime(startDate.getTime() - 1000 * 60 * 60); // subtract 1 hour to get a startpoint - - this._fetching = true; - const prefs = this._config!.prefs; + private async _getStatistics(energyData: EnergyData): Promise { const statistics: { to_grid?: string[]; from_grid?: string[]; solar?: string[]; } = {}; - for (const source of prefs.energy_sources) { + for (const source of energyData.prefs.energy_sources) { if (source.type === "solar") { if (statistics.solar) { statistics.solar.push(source.stat_energy_from); @@ -286,23 +226,17 @@ export class HuiEnergyUsageGraphCard } } - try { - this._data = await fetchStatistics( - this.hass!, - startDate, - undefined, - // Array.flat() - ([] as string[]).concat(...Object.values(statistics)) - ); - } finally { - this._fetching = false; - } - - const statisticsData = Object.values(this._data!); + const statisticsData = Object.values(energyData.stats); const datasets: ChartDataset<"bar">[] = []; let endTime: Date; + this._start = energyData.start; + this._end = energyData.end || endOfToday(); + if (statisticsData.length === 0) { + this._chartData = { + datasets, + }; return; } @@ -346,7 +280,7 @@ export class HuiEnergyUsageGraphCard const totalStats: { [start: string]: number } = {}; const sets: { [statId: string]: { [start: string]: number } } = {}; statIds!.forEach((id) => { - const stats = this._data![id]; + const stats = energyData.stats[id]; if (!stats) { return; } diff --git a/src/panels/lovelace/cards/hui-statistics-graph-card.ts b/src/panels/lovelace/cards/hui-statistics-graph-card.ts index 2b4e90522d..63058fe688 100644 --- a/src/panels/lovelace/cards/hui-statistics-graph-card.ts +++ b/src/panels/lovelace/cards/hui-statistics-graph-card.ts @@ -81,6 +81,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { ? processConfigEntities(config.entities) : []; + this._entities = []; configEntities.forEach((entity) => { this._entities.push(entity.entity); if (entity.name) { diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 4894837543..f1b5ff096b 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -1,4 +1,3 @@ -import { EnergyPreferences } from "../../../data/energy"; import { StatisticType } from "../../../data/history"; import { ActionConfig, LovelaceCardConfig } from "../../../data/lovelace"; import { FullCalendarView } from "../../../types"; @@ -93,54 +92,45 @@ export interface ButtonCardConfig extends LovelaceCardConfig { export interface EnergySummaryCardConfig extends LovelaceCardConfig { type: "energy-summary"; title?: string; - prefs: EnergyPreferences; } export interface EnergyDistributionCardConfig extends LovelaceCardConfig { type: "energy-distribution"; title?: string; - prefs: EnergyPreferences; } export interface EnergyUsageGraphCardConfig extends LovelaceCardConfig { type: "energy-summary-graph"; title?: string; - prefs: EnergyPreferences; } export interface EnergySolarGraphCardConfig extends LovelaceCardConfig { type: "energy-solar-graph"; title?: string; - prefs: EnergyPreferences; } export interface EnergyDevicesGraphCardConfig extends LovelaceCardConfig { type: "energy-devices-graph"; title?: string; - prefs: EnergyPreferences; } export interface EnergySourcesTableCardConfig extends LovelaceCardConfig { type: "energy-sources-table"; title?: string; - prefs: EnergyPreferences; } export interface EnergySolarGaugeCardConfig extends LovelaceCardConfig { type: "energy-solar-consumed-gauge"; title?: string; - prefs: EnergyPreferences; } export interface EnergyGridGaugeCardConfig extends LovelaceCardConfig { type: "energy-grid-result-gauge"; title?: string; - prefs: EnergyPreferences; } export interface EnergyCarbonGaugeCardConfig extends LovelaceCardConfig { type: "energy-carbon-consumed-gauge"; title?: string; - prefs: EnergyPreferences; } export interface EntityFilterCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts index 541e5202cb..7984ca2804 100644 --- a/src/panels/lovelace/components/hui-card-options.ts +++ b/src/panels/lovelace/components/hui-card-options.ts @@ -59,11 +59,12 @@ export class HuiCardOptions extends LitElement { )}
+ diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index e73a60619d..151f8b3638 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -51,6 +51,8 @@ const LAZY_LOAD_TYPES = { import("../cards/energy/hui-energy-grid-neutrality-gauge-card"), "energy-carbon-consumed-gauge": () => import("../cards/energy/hui-energy-carbon-consumed-gauge-card"), + "energy-date-selection": () => + import("../cards/energy/hui-energy-date-selection-card"), grid: () => import("../cards/hui-grid-card"), starting: () => import("../cards/hui-starting-card"), "entity-filter": () => import("../cards/hui-entity-filter-card"), diff --git a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts index a07e0a9fee..ab670415a6 100644 --- a/src/panels/lovelace/editor/view-editor/hui-view-editor.ts +++ b/src/panels/lovelace/editor/view-editor/hui-view-editor.ts @@ -3,7 +3,6 @@ import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { fireEvent } from "../../../../common/dom/fire_event"; import { slugify } from "../../../../common/string/slugify"; -import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import "../../../../components/ha-formfield"; import "../../../../components/ha-icon-input"; import "../../../../components/ha-switch"; @@ -59,11 +58,11 @@ export class HuiViewEditor extends LitElement { return this._config.theme || "Backend-selected"; } - get _panel(): boolean { + get _type(): string { if (!this._config) { - return false; + return "masonary"; } - return this._config.panel || false; + return this._config.panel ? "panel" : this._config.type || "masonary"; } set config(config: LovelaceViewConfig) { @@ -115,23 +114,26 @@ export class HuiViewEditor extends LitElement { .configValue=${"theme"} @value-changed=${this._valueChanged} > - - - - ${this.hass.localize( - "ui.panel.lovelace.editor.view.panel_mode.description" - )} - + + ${["masonary", "sidebar", "panel"].map( + (type) => html` + ${this.hass.localize( + `ui.panel.lovelace.editor.edit_view.types.${type}` + )} + ` + )} + +
`; } @@ -156,6 +158,23 @@ export class HuiViewEditor extends LitElement { fireEvent(this, "view-config-changed", { config: newConfig }); } + private _typeChanged(ev): void { + const selected = ev.target.selected; + if (selected === "") { + return; + } + const newConfig = { + ...this._config, + }; + delete newConfig.panel; + if (selected === "masonary") { + delete newConfig.type; + } else { + newConfig.type = selected; + } + fireEvent(this, "view-config-changed", { config: newConfig }); + } + private _handleTitleBlur(ev) { if ( !this.isNew || diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts index 0fe7dd3d2f..7b21e2a533 100644 --- a/src/panels/lovelace/views/hui-panel-view.ts +++ b/src/panels/lovelace/views/hui-panel-view.ts @@ -71,6 +71,13 @@ export class PanelView extends LitElement implements LovelaceViewElement { protected render(): TemplateResult { return html` + ${this.cards!.length > 1 + ? html` + ${this.hass!.localize( + "ui.panel.lovelace.editor.view.panel_mode.warning_multiple_cards" + )} + ` + : ""} ${this._card} ${this.lovelace?.editMode && this.cards.length === 0 ? html` @@ -117,18 +124,6 @@ export class PanelView extends LitElement implements LovelaceViewElement { card.editMode = true; wrapper.appendChild(card); this._card = wrapper; - - if (this.cards!.length > 1) { - const warning = document.createElement("hui-warning"); - warning.setAttribute( - "style", - "position: absolute; top: 0; width: 100%; box-sizing: border-box;" - ); - warning.innerText = this.hass!.localize( - "ui.panel.lovelace.editor.view.panel_mode.warning_multiple_cards" - ); - this._card = warning; - } } static get styles(): CSSResultGroup { diff --git a/src/panels/lovelace/views/hui-sidebar-view.ts b/src/panels/lovelace/views/hui-sidebar-view.ts index 5792b52b2b..f62acd7d7d 100644 --- a/src/panels/lovelace/views/hui-sidebar-view.ts +++ b/src/panels/lovelace/views/hui-sidebar-view.ts @@ -1,4 +1,4 @@ -import { mdiPlus } from "@mdi/js"; +import { mdiArrowLeft, mdiArrowRight, mdiPlus } from "@mdi/js"; import { css, CSSResultGroup, @@ -18,6 +18,7 @@ import type { import type { HomeAssistant } from "../../../types"; import { HuiErrorCard } from "../cards/hui-error-card"; import { HuiCardOptions } from "../components/hui-card-options"; +import { replaceCard } from "../editor/config-util"; import type { Lovelace, LovelaceCard } from "../types"; export class SideBarView extends LitElement implements LovelaceViewElement { @@ -155,6 +156,28 @@ export class SideBarView extends LitElement implements LovelaceViewElement { element.lovelace = this.lovelace; element.path = [this.index!, idx]; card.editMode = true; + const movePositionButton = document.createElement("mwc-icon-button"); + movePositionButton.slot = "buttons"; + const moveIcon = document.createElement("ha-svg-icon"); + moveIcon.path = + cardConfig?.view_layout?.position !== "sidebar" + ? mdiArrowRight + : mdiArrowLeft; + movePositionButton.appendChild(moveIcon); + movePositionButton.addEventListener("click", () => { + this.lovelace!.saveConfig( + replaceCard(this.lovelace!.config, [this.index!, idx], { + ...cardConfig!, + view_layout: { + position: + cardConfig?.view_layout?.position !== "sidebar" + ? "sidebar" + : "main", + }, + }) + ); + }); + element.appendChild(movePositionButton); element.appendChild(card); } if (cardConfig?.view_layout?.position !== "sidebar") { @@ -188,6 +211,7 @@ export class SideBarView extends LitElement implements LovelaceViewElement { #sidebar { flex-grow: 1; + flex-shrink: 0; max-width: 380px; } diff --git a/src/translations/en.json b/src/translations/en.json index 3b02d20f33..134baaea93 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -425,7 +425,13 @@ "date-range-picker": { "start_date": "Start date", "end_date": "End date", - "select": "Select" + "select": "Select", + "ranges": { + "today": "Today", + "yesterday": "Yesterday", + "this_week": "This week", + "last_week": "Last week" + } }, "relative_time": { "never": "Never", @@ -2807,22 +2813,6 @@ } } }, - "history": { - "ranges": { - "today": "Today", - "yesterday": "Yesterday", - "this_week": "This week", - "last_week": "Last week" - } - }, - "logbook": { - "ranges": { - "today": "Today", - "yesterday": "Yesterday", - "this_week": "This week", - "last_week": "Last week" - } - }, "lovelace": { "cards": { "confirm_delete": "Are you sure you want to delete this card?", @@ -2950,6 +2940,12 @@ "tab_visibility": "Visibility", "visibility": { "select_users": "Select which users should see this view in the navigation" + }, + "type": "View type", + "types": { + "masonary": "Masonary (default)", + "sidebar": "Sidebar", + "panel": "Panel (1 card)" } }, "edit_badges": { @@ -3243,8 +3239,6 @@ }, "view": { "panel_mode": { - "title": "Panel Mode?", - "description": "This renders the first card at full width. Other cards in this view as well as badges will not be rendered.", "warning_multiple_cards": "This view contains more than one card, but a panel view can only show 1 card." } },