From c34e8458861559f97af9363d5a67f381fa1ef990 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 15:34:25 -1000 Subject: [PATCH] Add support for streaming history --- src/data/history.ts | 30 ++--- .../lovelace/cards/hui-history-graph-card.ts | 118 +++++++++++------- 2 files changed, 87 insertions(+), 61 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index adab7e4d01..c8c0c9f95d 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -187,9 +187,8 @@ export const subscribeHistory = ( endTime: Date, entityIds: string[] ): Promise<() => Promise> => { - // If all specified filters are empty lists, we can return an empty list. const params = { - type: "history/history_during_period", + type: "history/stream", start_time: startTime.toISOString(), end_time: endTime.toISOString(), minimal_response: true, @@ -206,16 +205,13 @@ export const subscribeHistory = ( class HistoryStream { hass: HomeAssistant; - startTime: Date; - - endTime: Date; + hoursToShow: number; combinedHistory: HistoryStates; - constructor(hass: HomeAssistant, startTime: Date, endTime: Date) { + constructor(hass: HomeAssistant, hoursToShow: number) { this.hass = hass; - this.startTime = startTime; - this.endTime = endTime; + this.hoursToShow = hoursToShow; this.combinedHistory = {}; } @@ -231,7 +227,8 @@ class HistoryStream { // indicate no more historical events return this.combinedHistory; } - const purgeBeforePythonTime = this.startTime.getTime() / 1000; + const purgeBeforePythonTime = + (new Date().getTime() - 60 * 60 * this.hoursToShow * 1000) / 1000; const newHistory: HistoryStates = {}; Object.keys(streamMessage.states).forEach((entityId) => { newHistory[entityId] = []; @@ -260,24 +257,23 @@ class HistoryStream { } } -export const subscribeHistoryStates = ( +export const subscribeHistoryStatesWindow = ( hass: HomeAssistant, callbackFunction: (data: HistoryStates) => void, - startTime: Date, - endTime: Date, + hoursToShow: number, entityIds: string[] ): Promise<() => Promise> => { - // If all specified filters are empty lists, we can return an empty list. const params = { - type: "history/history_during_period", - start_time: startTime.toISOString(), - end_time: endTime.toISOString(), + type: "history/stream", + start_time: new Date( + new Date().getTime() - 60 * 60 * hoursToShow * 1000 + ).toISOString(), minimal_response: true, no_attributes: !entityIds.some((entityId) => entityIdHistoryNeedsAttributes(hass, entityId) ), }; - const stream = new HistoryStream(hass, startTime, endTime); + const stream = new HistoryStream(hass, hoursToShow); return hass.connection.subscribeMessage( (message) => callbackFunction(stream.processMessage(message)), params diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index c5bf22d9fb..c74bd69c68 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -8,11 +8,13 @@ import { } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; -import { throttle } from "../../../common/util/throttle"; import "../../../components/ha-card"; import "../../../components/chart/state-history-charts"; -import { CacheConfig, getRecentWithCache } from "../../../data/cached-history"; -import { HistoryResult } from "../../../data/history"; +import { + HistoryResult, + subscribeHistoryStatesWindow, + computeHistory, +} from "../../../data/history"; import { HomeAssistant } from "../../../types"; import { hasConfigOrEntitiesChanged } from "../common/has-changed"; import { processConfigEntities } from "../common/process-config-entities"; @@ -42,11 +44,13 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { private _names: Record = {}; - private _cacheConfig?: CacheConfig; + private _entityIds: string[] = []; - private _fetching = false; + private _hoursToShow = 24; - private _throttleGetStateHistory?: () => void; + private _error?: string; + + private _subscribed?: Promise<(() => Promise) | void>; public getCardSize(): number { return this._config?.title @@ -67,27 +71,71 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { ? processConfigEntities(config.entities) : []; - const _entities: string[] = []; - this._configEntities.forEach((entity) => { - _entities.push(entity.entity); + this._entityIds.push(entity.entity); if (entity.name) { this._names[entity.entity] = entity.name; } }); - this._throttleGetStateHistory = throttle(() => { - this._getStateHistory(); - }, config.refresh_interval || 10 * 1000); - - this._cacheConfig = { - cacheKey: _entities.join(), - hoursToShow: config.hours_to_show || 24, - }; + this._hoursToShow = config.hours_to_show || 24; this._config = config; } + public connectedCallback() { + super.connectedCallback(); + if (this.hasUpdated) { + this._subscribeHistoryWindow(); + } + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._unsubscribeHistoryWindow(); + } + + private _subscribeHistoryWindow() { + if (this._subscribed) { + return true; + } + this._subscribed = subscribeHistoryStatesWindow( + this.hass!, + (combinedHistory) => { + // "recent" means start time is a sliding window + // so we need to calculate an expireTime to + // purge old events + if (!this._subscribed) { + // Message came in before we had a chance to unload + return; + } + this._stateHistory = computeHistory( + this.hass!, + combinedHistory, + this.hass!.localize + ); + }, + this._hoursToShow, + this._entityIds + ).catch((err) => { + this._subscribed = undefined; + this._error = err; + }); + return true; + } + + private _unsubscribeHistoryWindow() { + if (!this._subscribed) { + return; + } + this._subscribed.then((unsubscribe) => { + if (unsubscribe) { + unsubscribe(); + } + this._subscribed = undefined; + }); + } + protected shouldUpdate(changedProps: PropertyValues): boolean { if (changedProps.has("_stateHistory")) { return true; @@ -100,8 +148,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { if ( !this._config || !this.hass || - !this._throttleGetStateHistory || - !this._cacheConfig + !this._hoursToShow || + !this._entityIds.length ) { return; } @@ -117,12 +165,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { if ( changedProps.has("_config") && (oldConfig?.entities !== this._config.entities || - oldConfig?.hours_to_show !== this._config.hours_to_show) + oldConfig?.hours_to_show !== this._hoursToShow) ) { - this._throttleGetStateHistory(); - } else if (changedProps.has("hass")) { - // wait for commit of data (we only account for the default setting of 1 sec) - setTimeout(this._throttleGetStateHistory, 1000); + this._unsubscribeHistoryWindow(); + this._subscribeHistoryWindow(); } } @@ -131,6 +177,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { return html``; } + if (this._error) { + return html`
${this._error}
`; + } + return html`
{ - if (this._fetching) { - return; - } - this._fetching = true; - try { - this._stateHistory = { - ...(await getRecentWithCache( - this.hass!, - this._configEntities!.map((config) => config.entity), - this._cacheConfig!, - this.hass!.localize, - this.hass!.language - )), - }; - } finally { - this._fetching = false; - } - } - static get styles(): CSSResultGroup { return css` ha-card {