From 21f58356bda380269ebcc77b7d8b14a07e32c2a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Jan 2023 09:27:05 -1000 Subject: [PATCH] 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 {