mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-04 06:57:47 +00:00
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
This commit is contained in:
parent
79cb7bda04
commit
21f58356bd
@ -1,4 +1,4 @@
|
|||||||
import { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
import { HassEntities } from "home-assistant-js-websocket";
|
||||||
import { LatLngTuple } from "leaflet";
|
import { LatLngTuple } from "leaflet";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
@ -12,11 +12,15 @@ import { customElement, property, query, state } from "lit/decorators";
|
|||||||
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
import { mdiImageFilterCenterFocus } from "@mdi/js";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
import { isToday } from "date-fns";
|
import { isToday } from "date-fns";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import { fetchRecent } from "../../../data/history";
|
import {
|
||||||
|
HistoryStates,
|
||||||
|
subscribeHistoryStatesTimeWindow,
|
||||||
|
} from "../../../data/history";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { processConfigEntities } from "../common/process-config-entities";
|
import { processConfigEntities } from "../common/process-config-entities";
|
||||||
@ -36,8 +40,7 @@ import {
|
|||||||
formatTimeWeekday,
|
formatTimeWeekday,
|
||||||
} from "../../../common/datetime/format_time";
|
} from "../../../common/datetime/format_time";
|
||||||
|
|
||||||
const MINUTE = 60000;
|
const DEFAULT_HOURS_TO_SHOW = 24;
|
||||||
|
|
||||||
@customElement("hui-map-card")
|
@customElement("hui-map-card")
|
||||||
class HuiMapCard extends LitElement implements LovelaceCard {
|
class HuiMapCard extends LitElement implements LovelaceCard {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -45,8 +48,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
@property({ type: Boolean, reflect: true })
|
@property({ type: Boolean, reflect: true })
|
||||||
public isPanel = false;
|
public isPanel = false;
|
||||||
|
|
||||||
@state()
|
@state() private _stateHistory?: HistoryStates;
|
||||||
private _history?: HassEntity[][];
|
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
private _config?: MapCardConfig;
|
private _config?: MapCardConfig;
|
||||||
@ -54,14 +56,16 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
@query("ha-map")
|
@query("ha-map")
|
||||||
private _map?: HaMap;
|
private _map?: HaMap;
|
||||||
|
|
||||||
private _date?: Date;
|
|
||||||
|
|
||||||
private _configEntities?: string[];
|
private _configEntities?: string[];
|
||||||
|
|
||||||
private _colorDict: Record<string, string> = {};
|
private _colorDict: Record<string, string> = {};
|
||||||
|
|
||||||
private _colorIndex = 0;
|
private _colorIndex = 0;
|
||||||
|
|
||||||
|
private _error?: string;
|
||||||
|
|
||||||
|
private _subscribed?: Promise<(() => Promise<void>) | void>;
|
||||||
|
|
||||||
public setConfig(config: MapCardConfig): void {
|
public setConfig(config: MapCardConfig): void {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
throw new Error("Error in card configuration.");
|
throw new Error("Error in card configuration.");
|
||||||
@ -88,8 +92,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
? processConfigEntities<EntityConfig>(config.entities)
|
? processConfigEntities<EntityConfig>(config.entities)
|
||||||
: []
|
: []
|
||||||
).map((entity) => entity.entity);
|
).map((entity) => entity.entity);
|
||||||
|
|
||||||
this._cleanupHistory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCardSize(): number {
|
public getCardSize(): number {
|
||||||
@ -133,6 +135,9 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
if (this._error) {
|
||||||
|
return html`<div class="error">${this._error}</div>`;
|
||||||
|
}
|
||||||
return html`
|
return html`
|
||||||
<ha-card id="card" .header=${this._config.title}>
|
<ha-card id="card" .header=${this._config.title}>
|
||||||
<div id="root">
|
<div id="root">
|
||||||
@ -144,7 +149,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
this._configEntities
|
this._configEntities
|
||||||
)}
|
)}
|
||||||
.zoom=${this._config.default_zoom ?? 14}
|
.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}
|
.autoFit=${this._config.auto_fit}
|
||||||
.darkMode=${this._config.dark_mode}
|
.darkMode=${this._config.dark_mode}
|
||||||
></ha-map>
|
></ha-map>
|
||||||
@ -176,23 +181,68 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any state has changed
|
if (changedProps.has("_stateHistory")) {
|
||||||
for (const entity of this._configEntities) {
|
return true;
|
||||||
if (oldHass.states[entity] !== this.hass!.states[entity]) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updated(changedProps: PropertyValues): void {
|
public connectedCallback() {
|
||||||
if (this._config?.hours_to_show && this._configEntities?.length) {
|
super.connectedCallback();
|
||||||
if (changedProps.has("_config")) {
|
if (this.hasUpdated && this._configEntities?.length) {
|
||||||
this._getHistory();
|
this._subscribeHistoryTimeWindow();
|
||||||
} else if (Date.now() - this._date!.getTime() >= MINUTE) {
|
}
|
||||||
this._getHistory();
|
}
|
||||||
|
|
||||||
|
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")) {
|
if (changedProps.has("_config")) {
|
||||||
this._computePadding();
|
this._computePadding();
|
||||||
@ -272,105 +322,53 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
|||||||
private _getHistoryPaths = memoizeOne(
|
private _getHistoryPaths = memoizeOne(
|
||||||
(
|
(
|
||||||
config: MapCardConfig,
|
config: MapCardConfig,
|
||||||
history?: HassEntity[][]
|
history?: HistoryStates
|
||||||
): HaMapPaths[] | undefined => {
|
): HaMapPaths[] | undefined => {
|
||||||
if (!config.hours_to_show || !history) {
|
if (!history) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const paths: HaMapPaths[] = [];
|
const paths: HaMapPaths[] = [];
|
||||||
|
|
||||||
for (const entityStates of history) {
|
Object.keys(history).forEach((entityId) => {
|
||||||
if (entityStates?.length <= 1) {
|
const entityStates = history[entityId];
|
||||||
continue;
|
if (entityStates?.length > 1) {
|
||||||
}
|
// filter location data from states and remove all invalid locations
|
||||||
// filter location data from states and remove all invalid locations
|
const points = entityStates.reduce(
|
||||||
const points = entityStates.reduce(
|
(accumulator: HaMapPathPoint[], entityState) => {
|
||||||
(accumulator: HaMapPathPoint[], entityState) => {
|
const latitude = entityState.a.latitude;
|
||||||
const latitude = entityState.attributes.latitude;
|
const longitude = entityState.a.longitude;
|
||||||
const longitude = entityState.attributes.longitude;
|
if (latitude && longitude) {
|
||||||
if (latitude && longitude) {
|
const p = {} as HaMapPathPoint;
|
||||||
const p = {} as HaMapPathPoint;
|
p.point = [latitude, longitude] as LatLngTuple;
|
||||||
p.point = [latitude, longitude] as LatLngTuple;
|
const t = new Date(entityState.lu * 1000);
|
||||||
const t = new Date(entityState.last_updated);
|
if (config.hours_to_show! || DEFAULT_HOURS_TO_SHOW > 144) {
|
||||||
if (config.hours_to_show! > 144) {
|
// if showing > 6 days in the history trail, show the full
|
||||||
// if showing > 6 days in the history trail, show the full
|
// date and time
|
||||||
// date and time
|
p.tooltip = formatDateTime(t, this.hass.locale);
|
||||||
p.tooltip = formatDateTime(t, this.hass.locale);
|
} else if (isToday(t)) {
|
||||||
} else if (isToday(t)) {
|
p.tooltip = formatTime(t, this.hass.locale);
|
||||||
p.tooltip = formatTime(t, this.hass.locale);
|
} else {
|
||||||
} else {
|
p.tooltip = formatTimeWeekday(t, this.hass.locale);
|
||||||
p.tooltip = formatTimeWeekday(t, this.hass.locale);
|
}
|
||||||
|
accumulator.push(p);
|
||||||
}
|
}
|
||||||
accumulator.push(p);
|
return accumulator;
|
||||||
}
|
},
|
||||||
return accumulator;
|
[]
|
||||||
},
|
) as HaMapPathPoint[];
|
||||||
[]
|
|
||||||
) as HaMapPathPoint[];
|
|
||||||
|
|
||||||
paths.push({
|
paths.push({
|
||||||
points,
|
points,
|
||||||
color: this._getColor(entityStates[0].entity_id),
|
color: this._getColor(entityId),
|
||||||
gradualOpacity: 0.8,
|
gradualOpacity: 0.8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
});
|
||||||
return paths;
|
return paths;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
private async _getHistory(): Promise<void> {
|
|
||||||
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 {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
ha-card {
|
ha-card {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user