diff --git a/src/data/cached-history.ts b/src/data/cached-history.ts deleted file mode 100644 index 2e4bf825f4..0000000000 --- a/src/data/cached-history.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { LocalizeFunc } from "../common/translations/localize"; -import { HomeAssistant } from "../types"; -import { - computeHistory, - HistoryStates, - HistoryResult, - LineChartUnit, - TimelineEntity, - entityIdHistoryNeedsAttributes, - fetchRecentWS, -} from "./history"; - -export interface CacheConfig { - cacheKey: string; - hoursToShow: number; -} - -interface CachedResults { - prom: Promise; - startTime: Date; - endTime: Date; - language: string; - data: HistoryResult; -} - -const stateHistoryCache: { [cacheKey: string]: CachedResults } = {}; - -// Cache type 2 functionality -function getEmptyCache( - language: string, - startTime: Date, - endTime: Date -): CachedResults { - return { - prom: Promise.resolve({ line: [], timeline: [] }), - language, - startTime, - endTime, - data: { line: [], timeline: [] }, - }; -} - -export const getRecentWithCache = ( - hass: HomeAssistant, - entityIds: string[], - cacheConfig: CacheConfig, - localize: LocalizeFunc, - language: string -) => { - const cacheKey = cacheConfig.cacheKey; - const fullCacheKey = cacheKey + `_${cacheConfig.hoursToShow}`; - const endTime = new Date(); - const startTime = new Date(endTime); - startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow); - let toFetchStartTime = startTime; - let appendingToCache = false; - - let cache = stateHistoryCache[fullCacheKey]; - if ( - cache && - toFetchStartTime >= cache.startTime && - toFetchStartTime <= cache.endTime && - cache.language === language - ) { - toFetchStartTime = cache.endTime; - appendingToCache = true; - // This pretty much never happens as endTime is usually set to now - if (endTime <= cache.endTime) { - return cache.prom; - } - } else { - cache = stateHistoryCache[fullCacheKey] = getEmptyCache( - language, - startTime, - endTime - ); - } - - const curCacheProm = cache.prom; - const noAttributes = !entityIds.some((entityId) => - entityIdHistoryNeedsAttributes(hass, entityId) - ); - - const genProm = async () => { - let fetchedHistory: HistoryStates; - - try { - const results = await Promise.all([ - curCacheProm, - fetchRecentWS( - hass, - entityIds, - toFetchStartTime, - endTime, - appendingToCache, - undefined, - true, - noAttributes - ), - ]); - fetchedHistory = results[1]; - } catch (err: any) { - delete stateHistoryCache[fullCacheKey]; - throw err; - } - const stateHistory = computeHistory(hass, fetchedHistory, localize); - if (appendingToCache) { - if (stateHistory.line.length) { - mergeLine(stateHistory.line, cache.data.line); - } - if (stateHistory.timeline.length) { - mergeTimeline(stateHistory.timeline, cache.data.timeline); - // Replace the timeline array to force an update - cache.data.timeline = [...cache.data.timeline]; - } - pruneStartTime(startTime, cache.data); - } else { - cache.data = stateHistory; - } - return cache.data; - }; - - cache.prom = genProm(); - cache.startTime = startTime; - cache.endTime = endTime; - return cache.prom; -}; - -const mergeLine = ( - historyLines: LineChartUnit[], - cacheLines: LineChartUnit[] -) => { - historyLines.forEach((line) => { - const unit = line.unit; - const oldLine = cacheLines.find((cacheLine) => cacheLine.unit === unit); - if (oldLine) { - line.data.forEach((entity) => { - const oldEntity = oldLine.data.find( - (cacheEntity) => entity.entity_id === cacheEntity.entity_id - ); - if (oldEntity) { - oldEntity.states = oldEntity.states.concat(entity.states); - } else { - oldLine.data.push(entity); - } - }); - // Replace the cached line data to force an update - oldLine.data = [...oldLine.data]; - } else { - cacheLines.push(line); - } - }); -}; - -const mergeTimeline = ( - historyTimelines: TimelineEntity[], - cacheTimelines: TimelineEntity[] -) => { - historyTimelines.forEach((timeline) => { - const oldTimeline = cacheTimelines.find( - (cacheTimeline) => cacheTimeline.entity_id === timeline.entity_id - ); - if (oldTimeline) { - oldTimeline.data = oldTimeline.data.concat(timeline.data); - } else { - cacheTimelines.push(timeline); - } - }); -}; - -const pruneArray = (originalStartTime: Date, arr) => { - if (arr.length === 0) { - return arr; - } - const changedAfterStartTime = arr.findIndex( - (state) => new Date(state.last_changed) > originalStartTime - ); - if (changedAfterStartTime === 0) { - // If all changes happened after originalStartTime then we are done. - return arr; - } - - // If all changes happened at or before originalStartTime. Use last index. - const updateIndex = - changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1; - arr[updateIndex].last_changed = originalStartTime; - return arr.slice(updateIndex); -}; - -const pruneStartTime = (originalStartTime: Date, cacheData: HistoryResult) => { - cacheData.line.forEach((line) => { - line.data.forEach((entity) => { - entity.states = pruneArray(originalStartTime, entity.states); - }); - }); - - cacheData.timeline.forEach((timeline) => { - timeline.data = pruneArray(originalStartTime, timeline.data); - }); -}; diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts index 73061a09b9..e6e03dc8c2 100644 --- a/src/dialogs/more-info/ha-more-info-history.ts +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -3,10 +3,12 @@ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; -import { throttle } from "../../common/util/throttle"; import "../../components/chart/state-history-charts"; -import { getRecentWithCache } from "../../data/cached-history"; -import { HistoryResult } from "../../data/history"; +import { + HistoryResult, + subscribeHistoryStatesTimeWindow, + computeHistory, +} from "../../data/history"; import { fetchStatistics, getStatisticMetadata, @@ -39,9 +41,11 @@ export class MoreInfoHistory extends LitElement { private _statNames?: Record; - private _throttleGetStateHistory = throttle(() => { - this._getStateHistory(); - }, 10000); + private _interval?: number; + + private _subscribed?: Promise<(() => Promise) | void>; + + private _error?: string; protected render(): TemplateResult { if (!this.entityId) { @@ -59,7 +63,9 @@ export class MoreInfoHistory extends LitElement { )} - ${this._statistics + ${this._error + ? html`
${this._error}
` + : this._statistics ? html` { + if (unsubscribe) { + unsubscribe(); + } + this._subscribed = undefined; + }); + } - if (this._statistics || !this.entityId || !changedProps.has("hass")) { - // Don't update statistics on a state update, as they only update every 5 minutes. - return; + private _redrawGraph() { + if (this._stateHistory) { + this._stateHistory = { ...this._stateHistory }; } + } - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - - if ( - oldHass && - this.hass.states[this.entityId] !== oldHass?.states[this.entityId] - ) { - // wait for commit of data (we only account for the default setting of 1 sec) - setTimeout(this._throttleGetStateHistory, 1000); - } + private _setRedrawTimer() { + // redraw the graph every minute to update the time axis + clearInterval(this._interval); + this._interval = window.setInterval(() => this._redrawGraph(), 1000 * 60); } private async _getStateHistory(): Promise { @@ -134,19 +161,29 @@ export class MoreInfoHistory extends LitElement { return; } } - if (!isComponentLoaded(this.hass, "history")) { + if (!isComponentLoaded(this.hass, "history") || this._subscribed) { return; } - this._stateHistory = await getRecentWithCache( + this._subscribed = subscribeHistoryStatesTimeWindow( this.hass!, - [this.entityId], - { - cacheKey: `more_info.${this.entityId}`, - hoursToShow: 24, + (combinedHistory) => { + if (!this._subscribed) { + // Message came in before we had a chance to unload + return; + } + this._stateHistory = computeHistory( + this.hass!, + combinedHistory, + this.hass!.localize + ); }, - this.hass!.localize, - this.hass!.language - ); + 24, + [this.entityId] + ).catch((err) => { + this._subscribed = undefined; + this._error = err; + }); + this._setRedrawTimer(); } private _close(): void { diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 78aba1108a..2e6df7db54 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -10,6 +10,7 @@ import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import "../../../components/ha-card"; import "../../../components/chart/state-history-charts"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { HistoryResult, subscribeHistoryStatesTimeWindow, @@ -98,7 +99,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { } private _subscribeHistoryTimeWindow() { - if (this._subscribed) { + if (!isComponentLoaded(this.hass!, "history") || this._subscribed) { return; } this._subscribed = subscribeHistoryStatesTimeWindow( @@ -176,7 +177,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { if ( changedProps.has("_config") && - (oldConfig?.entities !== this._config.entities || + (!this._subscribed || + oldConfig?.entities !== this._config.entities || oldConfig?.hours_to_show !== this._hoursToShow) ) { this._unsubscribeHistoryTimeWindow();