mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-11 03:51:07 +00:00
Compare commits
45 Commits
copilot/fi
...
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] ||
|
!hass.states[entityId] ||
|
||||||
NEED_ATTRIBUTE_DOMAINS.includes(computeDomain(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 = (
|
export const fetchDateWS = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
startTime: Date,
|
startTime: Date,
|
||||||
@@ -263,20 +196,27 @@ class HistoryStream {
|
|||||||
}
|
}
|
||||||
// Remove old history
|
// Remove old history
|
||||||
if (entityId in this.combinedHistory) {
|
if (entityId in this.combinedHistory) {
|
||||||
const entityHistory = newHistory[entityId];
|
const expiredStates = newHistory[entityId].filter(
|
||||||
while (entityHistory[0].lu < purgeBeforePythonTime) {
|
(state) => state.lu < purgeBeforePythonTime
|
||||||
if (entityHistory.length > 1) {
|
);
|
||||||
if (entityHistory[1].lu < purgeBeforePythonTime) {
|
if (!expiredStates.length) {
|
||||||
newHistory[entityId].shift();
|
continue;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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;
|
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 { 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,46 +322,44 @@ 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) {
|
for (const entityId of Object.keys(history)) {
|
||||||
if (entityStates?.length <= 1) {
|
const entityStates = history[entityId];
|
||||||
|
if (!entityStates?.length) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 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: HaMapPathPoint[] = [];
|
||||||
(accumulator: HaMapPathPoint[], entityState) => {
|
for (const entityState of entityStates) {
|
||||||
const latitude = entityState.attributes.latitude;
|
const latitude = entityState.a.latitude;
|
||||||
const longitude = entityState.attributes.longitude;
|
const longitude = entityState.a.longitude;
|
||||||
if (latitude && longitude) {
|
if (!latitude || !longitude) {
|
||||||
const p = {} as HaMapPathPoint;
|
continue;
|
||||||
p.point = [latitude, longitude] as LatLngTuple;
|
}
|
||||||
const t = new Date(entityState.last_updated);
|
const p = {} as HaMapPathPoint;
|
||||||
if (config.hours_to_show! > 144) {
|
p.point = [latitude, longitude] as LatLngTuple;
|
||||||
// if showing > 6 days in the history trail, show the full
|
const t = new Date(entityState.lu * 1000);
|
||||||
// date and time
|
if (config.hours_to_show! || DEFAULT_HOURS_TO_SHOW > 144) {
|
||||||
p.tooltip = formatDateTime(t, this.hass.locale);
|
// if showing > 6 days in the history trail, show the full
|
||||||
} else if (isToday(t)) {
|
// date and time
|
||||||
p.tooltip = formatTime(t, this.hass.locale);
|
p.tooltip = formatDateTime(t, this.hass.locale);
|
||||||
} else {
|
} else if (isToday(t)) {
|
||||||
p.tooltip = formatTimeWeekday(t, this.hass.locale);
|
p.tooltip = formatTime(t, this.hass.locale);
|
||||||
}
|
} else {
|
||||||
accumulator.push(p);
|
p.tooltip = formatTimeWeekday(t, this.hass.locale);
|
||||||
}
|
}
|
||||||
return accumulator;
|
points.push(p);
|
||||||
},
|
}
|
||||||
[]
|
|
||||||
) 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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -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 {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return css`
|
||||||
ha-card {
|
ha-card {
|
||||||
|
|||||||
Reference in New Issue
Block a user