diff --git a/src/panels/lovelace/common/graph/coordinates.ts b/src/panels/lovelace/common/graph/coordinates.ts index 7674492c9a..b63c61af05 100644 --- a/src/panels/lovelace/common/graph/coordinates.ts +++ b/src/panels/lovelace/common/graph/coordinates.ts @@ -1,4 +1,5 @@ import { strokeWidth } from "../../../../data/graph"; +import { EntityHistoryState } from "../../../../data/history"; const average = (items: any[]): number => items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / items.length; @@ -105,3 +106,25 @@ export const coordinates = ( return calcPoints(history, hours, width, detail, min, max); }; + +interface NumericEntityHistoryState { + state: number; + last_changed: number; +} + +export const coordinatesMinimalResponseCompressedState = ( + history: EntityHistoryState[], + hours: number, + width: number, + detail: number, + limits?: { min?: number; max?: number } +): number[][] | undefined => { + const numericHistory: NumericEntityHistoryState[] = history.map((item) => ({ + state: Number(item.s), + // With minimal response and compressed state, we don't have last_changed, + // so we use last_updated since its always the same as last_changed since + // we already filtered out states that are the same. + last_changed: item.lu * 1000, + })); + return coordinates(numericHistory, hours, width, detail, limits); +}; diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts index e3aa2ced83..12dbbfdcaf 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -9,18 +9,18 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import "../../../components/ha-circular-progress"; -import { fetchRecent } from "../../../data/history"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; +import { subscribeHistoryStatesTimeWindow } from "../../../data/history"; import { computeDomain } from "../../../common/entity/compute_domain"; import { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entities"; -import { coordinates } from "../common/graph/coordinates"; -import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { coordinatesMinimalResponseCompressedState } from "../common/graph/coordinates"; import "../components/hui-graph-base"; import { LovelaceHeaderFooter, LovelaceHeaderFooterEditor } from "../types"; import { GraphHeaderFooterConfig } from "./types"; const MINUTE = 60000; -const HOUR = MINUTE * 60; +const HOUR = 60 * MINUTE; const includeDomains = ["counter", "input_number", "number", "sensor"]; @customElement("hui-graph-header-footer") @@ -66,11 +66,11 @@ export class HuiGraphHeaderFooter @state() private _coordinates?: number[][]; - private _date?: Date; + private _error?: string; - private _stateHistory?: HassEntity[]; + private _interval?: number; - private _fetching = false; + private _subscribed?: Promise<(() => Promise) | void>; public getCardSize(): number { return 3; @@ -104,6 +104,10 @@ export class HuiGraphHeaderFooter return html``; } + if (this._error) { + return html`
${this._error}
`; + } + if (!this._coordinates) { return html`
@@ -125,89 +129,91 @@ export class HuiGraphHeaderFooter `; } - protected shouldUpdate(changedProps: PropertyValues): boolean { - return hasConfigOrEntityChanged(this, changedProps); + public connectedCallback() { + super.connectedCallback(); + if (this.hasUpdated) { + this._subscribeHistoryTimeWindow(); + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._unsubscribeHistoryTimeWindow(); + } + + private _subscribeHistoryTimeWindow() { + if (!isComponentLoaded(this.hass!, "history") || this._subscribed) { + return; + } + this._subscribed = subscribeHistoryStatesTimeWindow( + this.hass!, + (combinedHistory) => { + if (!this._subscribed) { + // Message came in before we had a chance to unload + return; + } + this._coordinates = + coordinatesMinimalResponseCompressedState( + combinedHistory[this._config!.entity], + this._config!.hours_to_show!, + 500, + this._config!.detail!, + this._config!.limits + ) || []; + }, + this._config!.hours_to_show!, + [this._config!.entity] + ).catch((err) => { + this._subscribed = undefined; + this._error = err; + }); + this._setRedrawTimer(); + } + + private _redrawGraph() { + if (this._coordinates) { + this._coordinates = [...this._coordinates]; + } + } + + private _setRedrawTimer() { + // redraw the graph every minute to update the time axis + clearInterval(this._interval); + this._interval = window.setInterval( + () => this._redrawGraph(), + this._config!.hours_to_show! > 24 ? HOUR : MINUTE + ); + } + + private _unsubscribeHistoryTimeWindow() { + clearInterval(this._interval); + if (!this._subscribed) { + return; + } + this._subscribed.then((unsubscribe) => { + if (unsubscribe) { + unsubscribe(); + } + this._subscribed = undefined; + }); } protected updated(changedProps: PropertyValues) { - if ( - !this._config || - !this.hass || - (this._fetching && !changedProps.has("_config")) - ) { + if (!this._config || !this.hass || !changedProps.has("_config")) { return; } - if (changedProps.has("_config")) { - const oldConfig = changedProps.get("_config") as GraphHeaderFooterConfig; - if (!oldConfig || oldConfig.entity !== this._config.entity) { - this._stateHistory = []; - } - - this._getCoordinates(); - } else if (Date.now() - this._date!.getTime() >= MINUTE) { - this._getCoordinates(); + const oldConfig = changedProps.get("_config") as GraphHeaderFooterConfig; + if ( + !oldConfig || + !this._subscribed || + oldConfig.entity !== this._config.entity + ) { + this._unsubscribeHistoryTimeWindow(); + this._subscribeHistoryTimeWindow(); } } - private async _getCoordinates(): Promise { - this._fetching = true; - const endTime = new Date(); - const startTime = - !this._date || !this._stateHistory?.length - ? new Date( - new Date().setHours( - endTime.getHours() - this._config!.hours_to_show! - ) - ) - : this._date; - - if (this._stateHistory!.length) { - const inHoursToShow: HassEntity[] = []; - const outHoursToShow: HassEntity[] = []; - // Split into inside and outside of "hours to show". - this._stateHistory!.forEach((entity) => - (endTime.getTime() - new Date(entity.last_changed).getTime() <= - this._config!.hours_to_show! * HOUR - ? inHoursToShow - : outHoursToShow - ).push(entity) - ); - - if (outHoursToShow.length) { - // If we have values that are now outside of "hours to show", re-add the last entry. This could e.g. be - // the "initial state" from the history backend. Without it, it would look like there is no history data - // at the start at all in the database = graph would start suddenly instead of on the left side of the card. - inHoursToShow.push(outHoursToShow[outHoursToShow.length - 1]); - } - this._stateHistory = inHoursToShow; - } - - const stateHistory = await fetchRecent( - this.hass!, - this._config!.entity, - startTime, - endTime, - Boolean(this._stateHistory!.length) - ); - - if (stateHistory.length && stateHistory[0].length) { - this._stateHistory!.push(...stateHistory[0]); - } - - this._coordinates = - coordinates( - this._stateHistory, - this._config!.hours_to_show!, - 500, - this._config!.detail!, - this._config!.limits - ) || []; - - this._date = endTime; - this._fetching = false; - } - static get styles(): CSSResultGroup { return css` ha-circular-progress {