From 1ec517237077af320616d180de1c4d043dda4e31 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 14:33:56 -1000 Subject: [PATCH 01/28] Add support for streaming history --- src/data/history.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/data/history.ts b/src/data/history.ts index 73688d4057..ee231fef69 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -79,6 +79,16 @@ interface EntityHistoryState { lu: number; } +interface StateDict { + [Key: string]: T; +} + +export interface HistoryStreamMessage { + states: StateDict; + start_time?: number; // Start time of this historical chunk + end_time?: number; // End time of this historical chunk +} + export const entityIdHistoryNeedsAttributes = ( hass: HomeAssistant, entityId: string @@ -174,6 +184,29 @@ export const fetchDateWS = ( return hass.callWS(params); }; +export const subscribeHistory = ( + hass: HomeAssistant, + callbackFunction: (message: HistoryStreamMessage) => void, + startTime: Date, + 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", + start_time: startTime.toISOString(), + end_time: endTime.toISOString(), + minimal_response: true, + no_attributes: !entityIds.some((entityId) => + entityIdHistoryNeedsAttributes(hass, entityId) + ), + }; + return hass.connection.subscribeMessage( + (message) => callbackFunction(message), + params + ); +}; + const equalState = (obj1: LineChartState, obj2: LineChartState) => obj1.state === obj2.state && // Only compare attributes if both states have an attributes object. From 5b3a13f8d4412de097e6a48897ecc3ca9ff3e116 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 14:35:06 -1000 Subject: [PATCH 02/28] Add support for streaming history --- src/data/history.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index ee231fef69..f0b145a8f0 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -79,12 +79,12 @@ interface EntityHistoryState { lu: number; } -interface StateDict { - [Key: string]: T; +interface EntityStateDict { + [entity_id: string]: T; } export interface HistoryStreamMessage { - states: StateDict; + states: EntityStateDict; start_time?: number; // Start time of this historical chunk end_time?: number; // End time of this historical chunk } From 412587a4572ac2c3eef7cc9e2c77bdd1d426973c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 15:06:47 -1000 Subject: [PATCH 03/28] Add support for streaming history --- src/data/history.ts | 87 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 82 insertions(+), 5 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index f0b145a8f0..adab7e4d01 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -79,12 +79,8 @@ interface EntityHistoryState { lu: number; } -interface EntityStateDict { - [entity_id: string]: T; -} - export interface HistoryStreamMessage { - states: EntityStateDict; + states: HistoryStates; start_time?: number; // Start time of this historical chunk end_time?: number; // End time of this historical chunk } @@ -207,6 +203,87 @@ export const subscribeHistory = ( ); }; +class HistoryStream { + hass: HomeAssistant; + + startTime: Date; + + endTime: Date; + + combinedHistory: HistoryStates; + + constructor(hass: HomeAssistant, startTime: Date, endTime: Date) { + this.hass = hass; + this.startTime = startTime; + this.endTime = endTime; + this.combinedHistory = {}; + } + + processMessage(streamMessage: HistoryStreamMessage): HistoryStates { + // Put newest ones on top. Reverse works in-place so + // make a copy first. + if (!this.combinedHistory || !this.combinedHistory.length) { + this.combinedHistory = streamMessage.states; + return this.combinedHistory; + } + if (!streamMessage.states.length) { + // Empty messages are still sent to + // indicate no more historical events + return this.combinedHistory; + } + const purgeBeforePythonTime = this.startTime.getTime() / 1000; + const newHistory: HistoryStates = {}; + Object.keys(streamMessage.states).forEach((entityId) => { + newHistory[entityId] = []; + }); + Object.keys(this.combinedHistory).forEach((entityId) => { + newHistory[entityId] = []; + }); + Object.keys(newHistory).forEach((entityId) => { + if ( + entityId in this.combinedHistory && + entityId in streamMessage.states + ) { + newHistory[entityId] = this.combinedHistory[entityId] + .concat(streamMessage.states[entityId]) + .filter((entry) => entry.lu > purgeBeforePythonTime); + } else if (entityId in this.combinedHistory) { + newHistory[entityId] = this.combinedHistory[entityId].filter( + (entry) => entry.lu > purgeBeforePythonTime + ); + } else { + newHistory[entityId] = streamMessage.states[entityId]; + } + }); + this.combinedHistory = newHistory; + return this.combinedHistory; + } +} + +export const subscribeHistoryStates = ( + hass: HomeAssistant, + callbackFunction: (data: HistoryStates) => void, + startTime: Date, + 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", + start_time: startTime.toISOString(), + end_time: endTime.toISOString(), + minimal_response: true, + no_attributes: !entityIds.some((entityId) => + entityIdHistoryNeedsAttributes(hass, entityId) + ), + }; + const stream = new HistoryStream(hass, startTime, endTime); + return hass.connection.subscribeMessage( + (message) => callbackFunction(stream.processMessage(message)), + params + ); +}; + const equalState = (obj1: LineChartState, obj2: LineChartState) => obj1.state === obj2.state && // Only compare attributes if both states have an attributes object. From c34e8458861559f97af9363d5a67f381fa1ef990 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 15:34:25 -1000 Subject: [PATCH 04/28] 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 { From 87d781f786ef353f6b651819fa6d1bc5ad1f0c78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 16:21:04 -1000 Subject: [PATCH 05/28] fixes --- src/data/history.ts | 51 +++++++++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index c8c0c9f95d..7a5bd11542 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -189,6 +189,7 @@ export const subscribeHistory = ( ): Promise<() => Promise> => { const params = { type: "history/stream", + entity_ids: entityIds, start_time: startTime.toISOString(), end_time: endTime.toISOString(), minimal_response: true, @@ -216,13 +217,11 @@ class HistoryStream { } processMessage(streamMessage: HistoryStreamMessage): HistoryStates { - // Put newest ones on top. Reverse works in-place so - // make a copy first. - if (!this.combinedHistory || !this.combinedHistory.length) { + if (!this.combinedHistory || !Object.keys(this.combinedHistory).length) { this.combinedHistory = streamMessage.states; return this.combinedHistory; } - if (!streamMessage.states.length) { + if (!Object.keys(streamMessage.states).length) { // Empty messages are still sent to // indicate no more historical events return this.combinedHistory; @@ -230,27 +229,52 @@ class HistoryStream { const purgeBeforePythonTime = (new Date().getTime() - 60 * 60 * this.hoursToShow * 1000) / 1000; const newHistory: HistoryStates = {}; - Object.keys(streamMessage.states).forEach((entityId) => { - newHistory[entityId] = []; - }); Object.keys(this.combinedHistory).forEach((entityId) => { newHistory[entityId] = []; }); + Object.keys(streamMessage.states).forEach((entityId) => { + newHistory[entityId] = []; + }); Object.keys(newHistory).forEach((entityId) => { + let purgeOld = false; if ( entityId in this.combinedHistory && entityId in streamMessage.states ) { - newHistory[entityId] = this.combinedHistory[entityId] - .concat(streamMessage.states[entityId]) - .filter((entry) => entry.lu > purgeBeforePythonTime); - } else if (entityId in this.combinedHistory) { - newHistory[entityId] = this.combinedHistory[entityId].filter( - (entry) => entry.lu > purgeBeforePythonTime + newHistory[entityId] = this.combinedHistory[entityId].concat( + streamMessage.states[entityId] ); + if ( + streamMessage.states[entityId][0] > this.combinedHistory[entityId][-1] + ) { + // If the history is out of order we have to sort it. + newHistory[entityId] = newHistory[entityId].sort( + (a, b) => a.lu - b.lu + ); + } + purgeOld = true; + } else if (entityId in this.combinedHistory) { + newHistory[entityId] = this.combinedHistory[entityId]; + purgeOld = true; } else { newHistory[entityId] = streamMessage.states[entityId]; } + if (purgeOld) { + const entityHistory = newHistory[entityId]; + while (entityHistory[0].lu < purgeBeforePythonTime) { + if (entityHistory.length > 1) { + if (entityHistory[1].lu < purgeBeforePythonTime) { + newHistory[entityId].shift(); + continue; + } + } + // Update the first entry to the start time state + // as we need to preserve the start time state and + // only expire the rest of the history as it ages. + entityHistory[0].lu = purgeBeforePythonTime; + break; + } + } }); this.combinedHistory = newHistory; return this.combinedHistory; @@ -265,6 +289,7 @@ export const subscribeHistoryStatesWindow = ( ): Promise<() => Promise> => { const params = { type: "history/stream", + entity_ids: entityIds, start_time: new Date( new Date().getTime() - 60 * 60 * hoursToShow * 1000 ).toISOString(), From c1e905e03a2c0792bb4b09c688b183ba59ecb4dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Jan 2023 16:30:39 -1000 Subject: [PATCH 06/28] cleanup --- src/panels/lovelace/cards/hui-history-graph-card.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index c74bd69c68..9c30190ce0 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -102,9 +102,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { 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; From 36710d758843a7a861c5957cd0b0371d17296cf9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Jan 2023 10:07:45 -1000 Subject: [PATCH 07/28] redraw --- .../lovelace/cards/hui-history-graph-card.ts | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index 9c30190ce0..f2efb1909b 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -50,6 +50,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { private _error?: string; + private _interval?: number; + private _subscribed?: Promise<(() => Promise) | void>; public getCardSize(): number { @@ -97,7 +99,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { private _subscribeHistoryWindow() { if (this._subscribed) { - return true; + return; } this._subscribed = subscribeHistoryStatesWindow( this.hass!, @@ -118,13 +120,26 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { this._subscribed = undefined; this._error = err; }); - return true; + this._setRedrawTimer(); + } + + private _redrawGraph() { + if (this._stateHistory) { + this._stateHistory = { ...this._stateHistory }; + } + } + + private _setRedrawTimer() { + // redraw the graph every minute to update the time axis + clearInterval(this._interval); + this._interval = window.setInterval(() => this._redrawGraph(), 1000 * 60); } private _unsubscribeHistoryWindow() { if (!this._subscribed) { return; } + clearInterval(this._interval); this._subscribed.then((unsubscribe) => { if (unsubscribe) { unsubscribe(); From bdcd1a0a8863668bb279f64e85be9cf1e72ea955 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Jan 2023 10:25:39 -1000 Subject: [PATCH 08/28] naming is hard --- src/data/history.ts | 2 +- .../lovelace/cards/hui-history-graph-card.ts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 7a5bd11542..9a0d2e2419 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -281,7 +281,7 @@ class HistoryStream { } } -export const subscribeHistoryStatesWindow = ( +export const subscribeHistoryStatesTimeWindow = ( hass: HomeAssistant, callbackFunction: (data: HistoryStates) => void, hoursToShow: number, diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts index f2efb1909b..78aba1108a 100644 --- a/src/panels/lovelace/cards/hui-history-graph-card.ts +++ b/src/panels/lovelace/cards/hui-history-graph-card.ts @@ -12,7 +12,7 @@ import "../../../components/ha-card"; import "../../../components/chart/state-history-charts"; import { HistoryResult, - subscribeHistoryStatesWindow, + subscribeHistoryStatesTimeWindow, computeHistory, } from "../../../data/history"; import { HomeAssistant } from "../../../types"; @@ -88,20 +88,20 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { public connectedCallback() { super.connectedCallback(); if (this.hasUpdated) { - this._subscribeHistoryWindow(); + this._subscribeHistoryTimeWindow(); } } public disconnectedCallback() { super.disconnectedCallback(); - this._unsubscribeHistoryWindow(); + this._unsubscribeHistoryTimeWindow(); } - private _subscribeHistoryWindow() { + private _subscribeHistoryTimeWindow() { if (this._subscribed) { return; } - this._subscribed = subscribeHistoryStatesWindow( + this._subscribed = subscribeHistoryStatesTimeWindow( this.hass!, (combinedHistory) => { if (!this._subscribed) { @@ -135,7 +135,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { this._interval = window.setInterval(() => this._redrawGraph(), 1000 * 60); } - private _unsubscribeHistoryWindow() { + private _unsubscribeHistoryTimeWindow() { if (!this._subscribed) { return; } @@ -179,8 +179,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { (oldConfig?.entities !== this._config.entities || oldConfig?.hours_to_show !== this._hoursToShow) ) { - this._unsubscribeHistoryWindow(); - this._subscribeHistoryWindow(); + this._unsubscribeHistoryTimeWindow(); + this._subscribeHistoryTimeWindow(); } } From c467ef82ea4042627d5d23756afedd2b4638f60a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 16 Jan 2023 12:26:23 -1000 Subject: [PATCH 09/28] drop cached history --- src/data/cached-history.ts | 200 ------------------ src/dialogs/more-info/ha-more-info-history.ts | 95 ++++++--- .../lovelace/cards/hui-history-graph-card.ts | 6 +- 3 files changed, 70 insertions(+), 231 deletions(-) delete mode 100644 src/data/cached-history.ts 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(); From ec844560afc685b1d678eedf78cdfedf3d80723d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jan 2023 11:27:05 -1000 Subject: [PATCH 10/28] backport --- src/data/history.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 9a0d2e2419..26ecabfccb 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -17,6 +17,8 @@ const NEED_ATTRIBUTE_DOMAINS = [ "input_datetime", "thermostat", "water_heater", + "person", + "device_tracker", ]; const LINE_ATTRIBUTES_TO_KEEP = [ "temperature", @@ -68,7 +70,7 @@ export interface HistoryStates { [entityId: string]: EntityHistoryState[]; } -interface EntityHistoryState { +export interface EntityHistoryState { /** state */ s: string; /** attributes */ @@ -285,7 +287,9 @@ export const subscribeHistoryStatesTimeWindow = ( hass: HomeAssistant, callbackFunction: (data: HistoryStates) => void, hoursToShow: number, - entityIds: string[] + entityIds: string[], + minimalResponse = true, + significantChangesOnly = true ): Promise<() => Promise> => { const params = { type: "history/stream", @@ -293,7 +297,8 @@ export const subscribeHistoryStatesTimeWindow = ( start_time: new Date( new Date().getTime() - 60 * 60 * hoursToShow * 1000 ).toISOString(), - minimal_response: true, + minimal_response: minimalResponse, + significant_changes_only: significantChangesOnly, no_attributes: !entityIds.some((entityId) => entityIdHistoryNeedsAttributes(hass, entityId) ), From 79cb7bda046157e9d2c325a9d78a7eef98059661 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 18 Jan 2023 19:00:39 -1000 Subject: [PATCH 11/28] Convert history header/footer to use streaming history needs #15112 --- .../lovelace/common/graph/coordinates.ts | 23 +++ .../header-footer/hui-graph-header-footer.ts | 168 +++++++++--------- 2 files changed, 110 insertions(+), 81 deletions(-) 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..f2264a3056 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() { + if (!this._subscribed) { + return; + } + clearInterval(this._interval); + 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 { From 21f58356bda380269ebcc77b7d8b14a07e32c2a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jan 2023 09:27:05 -1000 Subject: [PATCH 12/28] Update map card to use streaming history Update map card to use streaming history Update map card to use streaming history Update map card to use streaming history Update map card to use streaming history Update map card to use streaming history Update map card to use streaming history --- src/panels/lovelace/cards/hui-map-card.ts | 216 +++++++++++----------- 1 file changed, 107 insertions(+), 109 deletions(-) diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 412ccf8f13..3392f5d097 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -1,4 +1,4 @@ -import { HassEntities, HassEntity } from "home-assistant-js-websocket"; +import { HassEntities } from "home-assistant-js-websocket"; import { LatLngTuple } from "leaflet"; import { css, @@ -12,11 +12,15 @@ import { customElement, property, query, state } from "lit/decorators"; import { mdiImageFilterCenterFocus } from "@mdi/js"; import memoizeOne from "memoize-one"; import { isToday } from "date-fns"; +import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeDomain } from "../../../common/entity/compute_domain"; import parseAspectRatio from "../../../common/util/parse-aspect-ratio"; import "../../../components/ha-card"; import "../../../components/ha-icon-button"; -import { fetchRecent } from "../../../data/history"; +import { + HistoryStates, + subscribeHistoryStatesTimeWindow, +} from "../../../data/history"; import { HomeAssistant } from "../../../types"; import { findEntities } from "../common/find-entities"; import { processConfigEntities } from "../common/process-config-entities"; @@ -36,8 +40,7 @@ import { formatTimeWeekday, } from "../../../common/datetime/format_time"; -const MINUTE = 60000; - +const DEFAULT_HOURS_TO_SHOW = 24; @customElement("hui-map-card") class HuiMapCard extends LitElement implements LovelaceCard { @property({ attribute: false }) public hass!: HomeAssistant; @@ -45,8 +48,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { @property({ type: Boolean, reflect: true }) public isPanel = false; - @state() - private _history?: HassEntity[][]; + @state() private _stateHistory?: HistoryStates; @state() private _config?: MapCardConfig; @@ -54,14 +56,16 @@ class HuiMapCard extends LitElement implements LovelaceCard { @query("ha-map") private _map?: HaMap; - private _date?: Date; - private _configEntities?: string[]; private _colorDict: Record = {}; private _colorIndex = 0; + private _error?: string; + + private _subscribed?: Promise<(() => Promise) | void>; + public setConfig(config: MapCardConfig): void { if (!config) { throw new Error("Error in card configuration."); @@ -88,8 +92,6 @@ class HuiMapCard extends LitElement implements LovelaceCard { ? processConfigEntities(config.entities) : [] ).map((entity) => entity.entity); - - this._cleanupHistory(); } public getCardSize(): number { @@ -133,6 +135,9 @@ class HuiMapCard extends LitElement implements LovelaceCard { if (!this._config) { return html``; } + if (this._error) { + return html`
${this._error}
`; + } return html`
@@ -144,7 +149,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { this._configEntities )} .zoom=${this._config.default_zoom ?? 14} - .paths=${this._getHistoryPaths(this._config, this._history)} + .paths=${this._getHistoryPaths(this._config, this._stateHistory)} .autoFit=${this._config.auto_fit} .darkMode=${this._config.dark_mode} > @@ -176,23 +181,68 @@ class HuiMapCard extends LitElement implements LovelaceCard { return true; } - // Check if any state has changed - for (const entity of this._configEntities) { - if (oldHass.states[entity] !== this.hass!.states[entity]) { - return true; - } + if (changedProps.has("_stateHistory")) { + return true; } return false; } - protected updated(changedProps: PropertyValues): void { - if (this._config?.hours_to_show && this._configEntities?.length) { - if (changedProps.has("_config")) { - this._getHistory(); - } else if (Date.now() - this._date!.getTime() >= MINUTE) { - this._getHistory(); + public connectedCallback() { + super.connectedCallback(); + if (this.hasUpdated && this._configEntities?.length) { + 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._stateHistory = combinedHistory; + }, + this._config!.hours_to_show! || DEFAULT_HOURS_TO_SHOW, + this._configEntities!, + false, + false + ).catch((err) => { + this._subscribed = undefined; + this._error = err; + }); + } + + private _unsubscribeHistoryTimeWindow() { + if (!this._subscribed) { + return; + } + this._subscribed.then((unsubscribe) => { + if (unsubscribe) { + unsubscribe(); } + this._subscribed = undefined; + }); + } + + protected updated(changedProps: PropertyValues): void { + if (this._configEntities?.length) { + if (!this._subscribed || changedProps.has("_config")) { + this._unsubscribeHistoryTimeWindow(); + this._subscribeHistoryTimeWindow(); + } + } else { + this._unsubscribeHistoryTimeWindow(); } if (changedProps.has("_config")) { this._computePadding(); @@ -272,105 +322,53 @@ class HuiMapCard extends LitElement implements LovelaceCard { private _getHistoryPaths = memoizeOne( ( config: MapCardConfig, - history?: HassEntity[][] + history?: HistoryStates ): HaMapPaths[] | undefined => { - if (!config.hours_to_show || !history) { + if (!history) { return undefined; } const paths: HaMapPaths[] = []; - for (const entityStates of history) { - if (entityStates?.length <= 1) { - continue; - } - // filter location data from states and remove all invalid locations - const points = entityStates.reduce( - (accumulator: HaMapPathPoint[], entityState) => { - const latitude = entityState.attributes.latitude; - const longitude = entityState.attributes.longitude; - if (latitude && longitude) { - const p = {} as HaMapPathPoint; - p.point = [latitude, longitude] as LatLngTuple; - const t = new Date(entityState.last_updated); - if (config.hours_to_show! > 144) { - // if showing > 6 days in the history trail, show the full - // date and time - p.tooltip = formatDateTime(t, this.hass.locale); - } else if (isToday(t)) { - p.tooltip = formatTime(t, this.hass.locale); - } else { - p.tooltip = formatTimeWeekday(t, this.hass.locale); + Object.keys(history).forEach((entityId) => { + const entityStates = history[entityId]; + if (entityStates?.length > 1) { + // filter location data from states and remove all invalid locations + const points = entityStates.reduce( + (accumulator: HaMapPathPoint[], entityState) => { + const latitude = entityState.a.latitude; + const longitude = entityState.a.longitude; + if (latitude && longitude) { + const p = {} as HaMapPathPoint; + p.point = [latitude, longitude] as LatLngTuple; + const t = new Date(entityState.lu * 1000); + if (config.hours_to_show! || DEFAULT_HOURS_TO_SHOW > 144) { + // if showing > 6 days in the history trail, show the full + // date and time + p.tooltip = formatDateTime(t, this.hass.locale); + } else if (isToday(t)) { + p.tooltip = formatTime(t, this.hass.locale); + } else { + p.tooltip = formatTimeWeekday(t, this.hass.locale); + } + accumulator.push(p); } - accumulator.push(p); - } - return accumulator; - }, - [] - ) as HaMapPathPoint[]; + return accumulator; + }, + [] + ) as HaMapPathPoint[]; - paths.push({ - points, - color: this._getColor(entityStates[0].entity_id), - gradualOpacity: 0.8, - }); - } + paths.push({ + points, + color: this._getColor(entityId), + gradualOpacity: 0.8, + }); + } + }); return paths; } ); - private async _getHistory(): Promise { - this._date = new Date(); - - if (!this._configEntities) { - return; - } - - const entityIds = this._configEntities!.join(","); - const endTime = new Date(); - const startTime = new Date(); - startTime.setHours(endTime.getHours() - this._config!.hours_to_show!); - const skipInitialState = false; - const significantChangesOnly = false; - const minimalResponse = false; - - const stateHistory = await fetchRecent( - this.hass, - entityIds, - startTime, - endTime, - skipInitialState, - significantChangesOnly, - minimalResponse - ); - - if (stateHistory.length < 1) { - return; - } - this._history = stateHistory; - } - - private _cleanupHistory() { - if (!this._history) { - return; - } - if (this._config!.hours_to_show! <= 0) { - this._history = undefined; - } else { - // remove unused entities - this._history = this._history!.reduce( - (accumulator: HassEntity[][], entityStates) => { - const entityId = entityStates[0].entity_id; - if (this._configEntities?.includes(entityId)) { - accumulator.push(entityStates); - } - return accumulator; - }, - [] - ) as HassEntity[][]; - } - } - static get styles(): CSSResultGroup { return css` ha-card { From d57cf65edcd67fc49d2e4e0fc0809489ae9b7289 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jan 2023 14:36:31 -1000 Subject: [PATCH 13/28] Remove unused fetchRecent/fetchRecentWS/fetchDate history data functions These call are no longer needed after --- src/data/history.ts | 67 --------------------------------------------- 1 file changed, 67 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 26ecabfccb..389c66db8d 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -94,73 +94,6 @@ export const entityIdHistoryNeedsAttributes = ( !hass.states[entityId] || NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(entityId)); -export const fetchRecent = ( - hass: HomeAssistant, - entityId: string, - startTime: Date, - endTime: Date, - skipInitialState = false, - significantChangesOnly?: boolean, - minimalResponse = true, - noAttributes?: boolean -): Promise => { - let url = "history/period"; - if (startTime) { - url += "/" + startTime.toISOString(); - } - url += "?filter_entity_id=" + entityId; - if (endTime) { - url += "&end_time=" + endTime.toISOString(); - } - if (skipInitialState) { - url += "&skip_initial_state"; - } - if (significantChangesOnly !== undefined) { - url += `&significant_changes_only=${Number(significantChangesOnly)}`; - } - if (minimalResponse) { - url += "&minimal_response"; - } - if (noAttributes) { - url += "&no_attributes"; - } - return hass.callApi("GET", url); -}; - -export const fetchRecentWS = ( - hass: HomeAssistant, - entityIds: string[], - startTime: Date, - endTime: Date, - skipInitialState = false, - significantChangesOnly?: boolean, - minimalResponse = true, - noAttributes?: boolean -) => - hass.callWS({ - type: "history/history_during_period", - start_time: startTime.toISOString(), - end_time: endTime.toISOString(), - significant_changes_only: significantChangesOnly || false, - include_start_time_state: !skipInitialState, - minimal_response: minimalResponse, - no_attributes: noAttributes || false, - entity_ids: entityIds, - }); - -export const fetchDate = ( - hass: HomeAssistant, - startTime: Date, - endTime: Date, - entityIds: string[] -): Promise => - hass.callApi( - "GET", - `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${ - entityIds ? `&filter_entity_id=${entityIds.join(",")}` : `` - }` - ); - export const fetchDateWS = ( hass: HomeAssistant, startTime: Date, From 478539d58dff579ddc6b8231df88d6c3f0a93d09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 16:59:06 -1000 Subject: [PATCH 14/28] Update src/data/history.ts Co-authored-by: Paulus Schoutsen --- src/data/history.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 26ecabfccb..6fcb8ad9f1 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -231,9 +231,9 @@ class HistoryStream { const purgeBeforePythonTime = (new Date().getTime() - 60 * 60 * this.hoursToShow * 1000) / 1000; const newHistory: HistoryStates = {}; - Object.keys(this.combinedHistory).forEach((entityId) => { + for (const entityId of Object.keys(this.combinedHistory)) { newHistory[entityId] = []; - }); + }; Object.keys(streamMessage.states).forEach((entityId) => { newHistory[entityId] = []; }); From 4091205b58b3aec9e93a1a3475b7fe07f50d2b54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 16:59:33 -1000 Subject: [PATCH 15/28] Update src/data/history.ts Co-authored-by: Paulus Schoutsen --- src/data/history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/history.ts b/src/data/history.ts index 6fcb8ad9f1..27a1f6ada9 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -238,7 +238,7 @@ class HistoryStream { newHistory[entityId] = []; }); Object.keys(newHistory).forEach((entityId) => { - let purgeOld = false; + let purgeOld = entityId in this.combinedHistory; if ( entityId in this.combinedHistory && entityId in streamMessage.states From e9c5e51f251f99a829f1a6ef93fd67303a34eab0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:00:40 -1000 Subject: [PATCH 16/28] review --- src/data/history.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 27a1f6ada9..2e296c1c22 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -233,11 +233,11 @@ class HistoryStream { const newHistory: HistoryStates = {}; for (const entityId of Object.keys(this.combinedHistory)) { newHistory[entityId] = []; - }; - Object.keys(streamMessage.states).forEach((entityId) => { + } + for (const entityId of Object.keys(streamMessage.states)) { newHistory[entityId] = []; - }); - Object.keys(newHistory).forEach((entityId) => { + } + for (const entityId of Object.keys(newHistory)) { let purgeOld = entityId in this.combinedHistory; if ( entityId in this.combinedHistory && @@ -277,7 +277,7 @@ class HistoryStream { break; } } - }); + } this.combinedHistory = newHistory; return this.combinedHistory; } From e4ebb320e5cc2fd2b31d3b97d6fe94c1f6bbd8c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:01:23 -1000 Subject: [PATCH 17/28] review --- src/data/history.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 2e296c1c22..b717e0ed13 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -243,12 +243,11 @@ class HistoryStream { entityId in this.combinedHistory && entityId in streamMessage.states ) { + const lastCombined = this.combinedHistory[entityId][-1]; newHistory[entityId] = this.combinedHistory[entityId].concat( streamMessage.states[entityId] ); - if ( - streamMessage.states[entityId][0] > this.combinedHistory[entityId][-1] - ) { + if (streamMessage.states[entityId][0] > lastCombined) { // If the history is out of order we have to sort it. newHistory[entityId] = newHistory[entityId].sort( (a, b) => a.lu - b.lu From 51a69f10422f1160db04b55259b975525567fbd4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:02:15 -1000 Subject: [PATCH 18/28] review --- src/data/history.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index b717e0ed13..035ec714c1 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -243,11 +243,12 @@ class HistoryStream { entityId in this.combinedHistory && entityId in streamMessage.states ) { - const lastCombined = this.combinedHistory[entityId][-1]; - newHistory[entityId] = this.combinedHistory[entityId].concat( + const entityCombinedHistory = this.combinedHistory[entityId]; + const lastEntityCombinedHistory = entityCombinedHistory[-1]; + newHistory[entityId] = entityCombinedHistory.concat( streamMessage.states[entityId] ); - if (streamMessage.states[entityId][0] > lastCombined) { + if (streamMessage.states[entityId][0] > lastEntityCombinedHistory) { // If the history is out of order we have to sort it. newHistory[entityId] = newHistory[entityId].sort( (a, b) => a.lu - b.lu From 45316b458f6db694d0054eaade0d9124eb9ea646 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:03:18 -1000 Subject: [PATCH 19/28] review --- src/data/history.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/history.ts b/src/data/history.ts index 035ec714c1..51f313ec83 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -244,7 +244,8 @@ class HistoryStream { entityId in streamMessage.states ) { const entityCombinedHistory = this.combinedHistory[entityId]; - const lastEntityCombinedHistory = entityCombinedHistory[-1]; + const lastEntityCombinedHistory = + entityCombinedHistory[entityCombinedHistory.length - 1]; newHistory[entityId] = entityCombinedHistory.concat( streamMessage.states[entityId] ); From e1d17f3d3ab8d3c51d453b6800b311b19fa4df91 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:03:39 -1000 Subject: [PATCH 20/28] review --- src/data/history.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/data/history.ts b/src/data/history.ts index 51f313ec83..216f764b29 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -249,7 +249,9 @@ class HistoryStream { newHistory[entityId] = entityCombinedHistory.concat( streamMessage.states[entityId] ); - if (streamMessage.states[entityId][0] > lastEntityCombinedHistory) { + if ( + streamMessage.states[entityId][0].lu > lastEntityCombinedHistory.lu + ) { // If the history is out of order we have to sort it. newHistory[entityId] = newHistory[entityId].sort( (a, b) => a.lu - b.lu From 690e2a3aa95cd8a180fc46602e3b560a26769cd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:06:23 -1000 Subject: [PATCH 21/28] review --- src/data/history.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 216f764b29..2c2e93c609 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -238,7 +238,6 @@ class HistoryStream { newHistory[entityId] = []; } for (const entityId of Object.keys(newHistory)) { - let purgeOld = entityId in this.combinedHistory; if ( entityId in this.combinedHistory && entityId in streamMessage.states @@ -257,14 +256,13 @@ class HistoryStream { (a, b) => a.lu - b.lu ); } - purgeOld = true; } else if (entityId in this.combinedHistory) { newHistory[entityId] = this.combinedHistory[entityId]; - purgeOld = true; } else { newHistory[entityId] = streamMessage.states[entityId]; } - if (purgeOld) { + // Remove old history + if (entityId in this.combinedHistory) { const entityHistory = newHistory[entityId]; while (entityHistory[0].lu < purgeBeforePythonTime) { if (entityHistory.length > 1) { From 1e4a3c398bf83ba0eee999ab9e7fd2ac773538de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:08:03 -1000 Subject: [PATCH 22/28] review --- src/data/history.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/history.ts b/src/data/history.ts index 2c2e93c609..1f95af2552 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -249,7 +249,7 @@ class HistoryStream { streamMessage.states[entityId] ); if ( - streamMessage.states[entityId][0].lu > lastEntityCombinedHistory.lu + streamMessage.states[entityId][0].lu < lastEntityCombinedHistory.lu ) { // If the history is out of order we have to sort it. newHistory[entityId] = newHistory[entityId].sort( From 9f9ab6c23889291ed1af9a248c4242ce033f9796 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:10:55 -1000 Subject: [PATCH 23/28] review --- src/dialogs/more-info/ha-more-info-history.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dialogs/more-info/ha-more-info-history.ts b/src/dialogs/more-info/ha-more-info-history.ts index e6e03dc8c2..dd2a5c1165 100644 --- a/src/dialogs/more-info/ha-more-info-history.ts +++ b/src/dialogs/more-info/ha-more-info-history.ts @@ -164,6 +164,9 @@ export class MoreInfoHistory extends LitElement { if (!isComponentLoaded(this.hass, "history") || this._subscribed) { return; } + if (this._subscribed) { + this._unsubscribeHistoryTimeWindow(); + } this._subscribed = subscribeHistoryStatesTimeWindow( this.hass!, (combinedHistory) => { From 6ba31da4a5a619a0da1bfbcfe18723de595e19aa Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:17:43 -1000 Subject: [PATCH 24/28] adjust --- src/data/history.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index 1f95af2552..cb5977778c 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -264,19 +264,18 @@ class HistoryStream { // Remove old history if (entityId in this.combinedHistory) { const entityHistory = newHistory[entityId]; - while (entityHistory[0].lu < purgeBeforePythonTime) { - if (entityHistory.length > 1) { - if (entityHistory[1].lu < purgeBeforePythonTime) { - newHistory[entityId].shift(); - continue; - } - } - // Update the first entry to the start time state - // as we need to preserve the start time state and - // only expire the rest of the history as it ages. - entityHistory[0].lu = purgeBeforePythonTime; - break; + const lastEntry = entityHistory[entityHistory.length - 1]; + const filtered = entityHistory.filter( + (ent) => ent.lu < purgeBeforePythonTime + ); + if (filtered.length === 0) { + filtered.push(lastEntry); } + // Update the first entry to the start time state + // as we need to preserve the start time state and + // only expire the rest of the history as it ages. + filtered[0].lu = purgeBeforePythonTime; + newHistory[entityId] = filtered; } } this.combinedHistory = newHistory; From 7923c172ff0440f53dcf9d900d4a85962f3b07b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:28:42 -1000 Subject: [PATCH 25/28] Revert "adjust" This reverts commit 6ba31da4a5a619a0da1bfbcfe18723de595e19aa. --- src/data/history.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/data/history.ts b/src/data/history.ts index cb5977778c..1f95af2552 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -264,18 +264,19 @@ class HistoryStream { // Remove old history if (entityId in this.combinedHistory) { const entityHistory = newHistory[entityId]; - const lastEntry = entityHistory[entityHistory.length - 1]; - const filtered = entityHistory.filter( - (ent) => ent.lu < purgeBeforePythonTime - ); - if (filtered.length === 0) { - filtered.push(lastEntry); + while (entityHistory[0].lu < purgeBeforePythonTime) { + if (entityHistory.length > 1) { + if (entityHistory[1].lu < purgeBeforePythonTime) { + newHistory[entityId].shift(); + continue; + } + } + // Update the first entry to the start time state + // as we need to preserve the start time state and + // only expire the rest of the history as it ages. + entityHistory[0].lu = purgeBeforePythonTime; + break; } - // Update the first entry to the start time state - // as we need to preserve the start time state and - // only expire the rest of the history as it ages. - filtered[0].lu = purgeBeforePythonTime; - newHistory[entityId] = filtered; } } this.combinedHistory = newHistory; From 265ca40df1525f97c6819a01cd2865a75da73dd4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:45:29 -1000 Subject: [PATCH 26/28] move setInterval --- src/panels/lovelace/header-footer/hui-graph-header-footer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f2264a3056..12dbbfdcaf 100644 --- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts +++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts @@ -186,10 +186,10 @@ export class HuiGraphHeaderFooter } private _unsubscribeHistoryTimeWindow() { + clearInterval(this._interval); if (!this._subscribed) { return; } - clearInterval(this._interval); this._subscribed.then((unsubscribe) => { if (unsubscribe) { unsubscribe(); From 26730cb2e2541579b47c5f389a3f58a9cb0f717f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:50:22 -1000 Subject: [PATCH 27/28] Object.keys to for --- src/panels/lovelace/cards/hui-map-card.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 3392f5d097..08ba908faa 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -330,7 +330,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { const paths: HaMapPaths[] = []; - Object.keys(history).forEach((entityId) => { + for (const entityId of Object.keys(history)) { const entityStates = history[entityId]; if (entityStates?.length > 1) { // filter location data from states and remove all invalid locations @@ -364,7 +364,7 @@ class HuiMapCard extends LitElement implements LovelaceCard { gradualOpacity: 0.8, }); } - }); + } return paths; } ); From cfdbad504c9dfb3335ebfb77b94d9734a6993d95 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Jan 2023 17:53:38 -1000 Subject: [PATCH 28/28] refactor --- src/panels/lovelace/cards/hui-map-card.ts | 60 +++++++++++------------ 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/src/panels/lovelace/cards/hui-map-card.ts b/src/panels/lovelace/cards/hui-map-card.ts index 08ba908faa..b608e61b3a 100644 --- a/src/panels/lovelace/cards/hui-map-card.ts +++ b/src/panels/lovelace/cards/hui-map-card.ts @@ -332,38 +332,36 @@ class HuiMapCard extends LitElement implements LovelaceCard { for (const entityId of Object.keys(history)) { const entityStates = history[entityId]; - if (entityStates?.length > 1) { - // filter location data from states and remove all invalid locations - const points = entityStates.reduce( - (accumulator: HaMapPathPoint[], entityState) => { - const latitude = entityState.a.latitude; - const longitude = entityState.a.longitude; - if (latitude && longitude) { - const p = {} as HaMapPathPoint; - p.point = [latitude, longitude] as LatLngTuple; - const t = new Date(entityState.lu * 1000); - if (config.hours_to_show! || DEFAULT_HOURS_TO_SHOW > 144) { - // if showing > 6 days in the history trail, show the full - // date and time - p.tooltip = formatDateTime(t, this.hass.locale); - } else if (isToday(t)) { - p.tooltip = formatTime(t, this.hass.locale); - } else { - p.tooltip = formatTimeWeekday(t, this.hass.locale); - } - accumulator.push(p); - } - return accumulator; - }, - [] - ) as HaMapPathPoint[]; - - paths.push({ - points, - color: this._getColor(entityId), - gradualOpacity: 0.8, - }); + if (!entityStates?.length) { + continue; } + // filter location data from states and remove all invalid locations + const points: HaMapPathPoint[] = []; + for (const entityState of entityStates) { + const latitude = entityState.a.latitude; + const longitude = entityState.a.longitude; + if (!latitude || !longitude) { + continue; + } + const p = {} as HaMapPathPoint; + p.point = [latitude, longitude] as LatLngTuple; + const t = new Date(entityState.lu * 1000); + if (config.hours_to_show! || DEFAULT_HOURS_TO_SHOW > 144) { + // if showing > 6 days in the history trail, show the full + // date and time + p.tooltip = formatDateTime(t, this.hass.locale); + } else if (isToday(t)) { + p.tooltip = formatTime(t, this.hass.locale); + } else { + p.tooltip = formatTimeWeekday(t, this.hass.locale); + } + points.push(p); + } + paths.push({ + points, + color: this._getColor(entityId), + gradualOpacity: 0.8, + }); } return paths; }