mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-18 17:39:48 +00:00
Compare commits
45 Commits
20231027.0
...
int2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d154872955 | ||
![]() |
555f0dbc81 | ||
![]() |
076c58acea | ||
![]() |
c873ea0bf8 | ||
![]() |
73b49642f8 | ||
![]() |
cfdbad504c | ||
![]() |
26730cb2e2 | ||
![]() |
a77589922c | ||
![]() |
545f81fa24 | ||
![]() |
265ca40df1 | ||
![]() |
58cb775627 | ||
![]() |
945d992884 | ||
![]() |
574396f369 | ||
![]() |
42d3adad26 | ||
![]() |
7923c172ff | ||
![]() |
ced00a4945 | ||
![]() |
9c2a72d240 | ||
![]() |
89c2db1f8e | ||
![]() |
6ba31da4a5 | ||
![]() |
1c516822a5 | ||
![]() |
be741feb2a | ||
![]() |
72605051e4 | ||
![]() |
9f9ab6c238 | ||
![]() |
1e4a3c398b | ||
![]() |
690e2a3aa9 | ||
![]() |
e1d17f3d3a | ||
![]() |
45316b458f | ||
![]() |
51a69f1042 | ||
![]() |
e4ebb320e5 | ||
![]() |
e9c5e51f25 | ||
![]() |
4091205b58 | ||
![]() |
478539d58d | ||
![]() |
d57cf65edc | ||
![]() |
21f58356bd | ||
![]() |
79cb7bda04 | ||
![]() |
ec844560af | ||
![]() |
c467ef82ea | ||
![]() |
bdcd1a0a88 | ||
![]() |
36710d7588 | ||
![]() |
c1e905e03a | ||
![]() |
87d781f786 | ||
![]() |
c34e845886 | ||
![]() |
412587a457 | ||
![]() |
5b3a13f8d4 | ||
![]() |
1ec5172370 |
@@ -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<HassEntity[][]> => {
|
||||
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<HistoryStates>({
|
||||
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<HassEntity[][]> =>
|
||||
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,
|
||||
@@ -263,20 +196,27 @@ 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 expiredStates = newHistory[entityId].filter(
|
||||
(state) => state.lu < purgeBeforePythonTime
|
||||
);
|
||||
if (!expiredStates.length) {
|
||||
continue;
|
||||
}
|
||||
newHistory[entityId] = newHistory[entityId].filter(
|
||||
(state) => state.lu >= purgeBeforePythonTime
|
||||
);
|
||||
if (
|
||||
newHistory[entityId].length &&
|
||||
newHistory[entityId][0].lu === purgeBeforePythonTime
|
||||
) {
|
||||
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.
|
||||
const lastExpiredState = expiredStates[expiredStates.length - 1];
|
||||
lastExpiredState.lu = purgeBeforePythonTime;
|
||||
newHistory[entityId].unshift(lastExpiredState);
|
||||
}
|
||||
}
|
||||
this.combinedHistory = newHistory;
|
||||
|
@@ -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<string, string> = {};
|
||||
|
||||
private _colorIndex = 0;
|
||||
|
||||
private _error?: string;
|
||||
|
||||
private _subscribed?: Promise<(() => Promise<void>) | 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<EntityConfig>(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`<div class="error">${this._error}</div>`;
|
||||
}
|
||||
return html`
|
||||
<ha-card id="card" .header=${this._config.title}>
|
||||
<div id="root">
|
||||
@@ -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}
|
||||
></ha-map>
|
||||
@@ -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,46 +322,44 @@ 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) {
|
||||
for (const entityId of Object.keys(history)) {
|
||||
const entityStates = history[entityId];
|
||||
if (!entityStates?.length) {
|
||||
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);
|
||||
}
|
||||
accumulator.push(p);
|
||||
}
|
||||
return accumulator;
|
||||
},
|
||||
[]
|
||||
) as HaMapPathPoint[];
|
||||
|
||||
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(entityStates[0].entity_id),
|
||||
color: this._getColor(entityId),
|
||||
gradualOpacity: 0.8,
|
||||
});
|
||||
}
|
||||
@@ -319,58 +367,6 @@ class HuiMapCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
);
|
||||
|
||||
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 {
|
||||
return css`
|
||||
ha-card {
|
||||
|
Reference in New Issue
Block a user