Add support for streaming history

This commit is contained in:
J. Nick Koston 2023-01-15 15:34:25 -10:00
parent 412587a457
commit c34e845886
2 changed files with 87 additions and 61 deletions

View File

@ -187,9 +187,8 @@ export const subscribeHistory = (
endTime: Date, endTime: Date,
entityIds: string[] entityIds: string[]
): Promise<() => Promise<void>> => { ): Promise<() => Promise<void>> => {
// If all specified filters are empty lists, we can return an empty list.
const params = { const params = {
type: "history/history_during_period", type: "history/stream",
start_time: startTime.toISOString(), start_time: startTime.toISOString(),
end_time: endTime.toISOString(), end_time: endTime.toISOString(),
minimal_response: true, minimal_response: true,
@ -206,16 +205,13 @@ export const subscribeHistory = (
class HistoryStream { class HistoryStream {
hass: HomeAssistant; hass: HomeAssistant;
startTime: Date; hoursToShow: number;
endTime: Date;
combinedHistory: HistoryStates; combinedHistory: HistoryStates;
constructor(hass: HomeAssistant, startTime: Date, endTime: Date) { constructor(hass: HomeAssistant, hoursToShow: number) {
this.hass = hass; this.hass = hass;
this.startTime = startTime; this.hoursToShow = hoursToShow;
this.endTime = endTime;
this.combinedHistory = {}; this.combinedHistory = {};
} }
@ -231,7 +227,8 @@ class HistoryStream {
// indicate no more historical events // indicate no more historical events
return this.combinedHistory; return this.combinedHistory;
} }
const purgeBeforePythonTime = this.startTime.getTime() / 1000; const purgeBeforePythonTime =
(new Date().getTime() - 60 * 60 * this.hoursToShow * 1000) / 1000;
const newHistory: HistoryStates = {}; const newHistory: HistoryStates = {};
Object.keys(streamMessage.states).forEach((entityId) => { Object.keys(streamMessage.states).forEach((entityId) => {
newHistory[entityId] = []; newHistory[entityId] = [];
@ -260,24 +257,23 @@ class HistoryStream {
} }
} }
export const subscribeHistoryStates = ( export const subscribeHistoryStatesWindow = (
hass: HomeAssistant, hass: HomeAssistant,
callbackFunction: (data: HistoryStates) => void, callbackFunction: (data: HistoryStates) => void,
startTime: Date, hoursToShow: number,
endTime: Date,
entityIds: string[] entityIds: string[]
): Promise<() => Promise<void>> => { ): Promise<() => Promise<void>> => {
// If all specified filters are empty lists, we can return an empty list.
const params = { const params = {
type: "history/history_during_period", type: "history/stream",
start_time: startTime.toISOString(), start_time: new Date(
end_time: endTime.toISOString(), new Date().getTime() - 60 * 60 * hoursToShow * 1000
).toISOString(),
minimal_response: true, minimal_response: true,
no_attributes: !entityIds.some((entityId) => no_attributes: !entityIds.some((entityId) =>
entityIdHistoryNeedsAttributes(hass, entityId) entityIdHistoryNeedsAttributes(hass, entityId)
), ),
}; };
const stream = new HistoryStream(hass, startTime, endTime); const stream = new HistoryStream(hass, hoursToShow);
return hass.connection.subscribeMessage<HistoryStreamMessage>( return hass.connection.subscribeMessage<HistoryStreamMessage>(
(message) => callbackFunction(stream.processMessage(message)), (message) => callbackFunction(stream.processMessage(message)),
params params

View File

@ -8,11 +8,13 @@ import {
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { throttle } from "../../../common/util/throttle";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/chart/state-history-charts"; import "../../../components/chart/state-history-charts";
import { CacheConfig, getRecentWithCache } from "../../../data/cached-history"; import {
import { HistoryResult } from "../../../data/history"; HistoryResult,
subscribeHistoryStatesWindow,
computeHistory,
} from "../../../data/history";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { hasConfigOrEntitiesChanged } from "../common/has-changed"; import { hasConfigOrEntitiesChanged } from "../common/has-changed";
import { processConfigEntities } from "../common/process-config-entities"; import { processConfigEntities } from "../common/process-config-entities";
@ -42,11 +44,13 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
private _names: Record<string, string> = {}; private _names: Record<string, string> = {};
private _cacheConfig?: CacheConfig; private _entityIds: string[] = [];
private _fetching = false; private _hoursToShow = 24;
private _throttleGetStateHistory?: () => void; private _error?: string;
private _subscribed?: Promise<(() => Promise<void>) | void>;
public getCardSize(): number { public getCardSize(): number {
return this._config?.title return this._config?.title
@ -67,27 +71,71 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
? processConfigEntities(config.entities) ? processConfigEntities(config.entities)
: []; : [];
const _entities: string[] = [];
this._configEntities.forEach((entity) => { this._configEntities.forEach((entity) => {
_entities.push(entity.entity); this._entityIds.push(entity.entity);
if (entity.name) { if (entity.name) {
this._names[entity.entity] = entity.name; this._names[entity.entity] = entity.name;
} }
}); });
this._throttleGetStateHistory = throttle(() => { this._hoursToShow = config.hours_to_show || 24;
this._getStateHistory();
}, config.refresh_interval || 10 * 1000);
this._cacheConfig = {
cacheKey: _entities.join(),
hoursToShow: config.hours_to_show || 24,
};
this._config = config; 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 { protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_stateHistory")) { if (changedProps.has("_stateHistory")) {
return true; return true;
@ -100,8 +148,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
if ( if (
!this._config || !this._config ||
!this.hass || !this.hass ||
!this._throttleGetStateHistory || !this._hoursToShow ||
!this._cacheConfig !this._entityIds.length
) { ) {
return; return;
} }
@ -117,12 +165,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
if ( if (
changedProps.has("_config") && changedProps.has("_config") &&
(oldConfig?.entities !== this._config.entities || (oldConfig?.entities !== this._config.entities ||
oldConfig?.hours_to_show !== this._config.hours_to_show) oldConfig?.hours_to_show !== this._hoursToShow)
) { ) {
this._throttleGetStateHistory(); this._unsubscribeHistoryWindow();
} else if (changedProps.has("hass")) { this._subscribeHistoryWindow();
// wait for commit of data (we only account for the default setting of 1 sec)
setTimeout(this._throttleGetStateHistory, 1000);
} }
} }
@ -131,6 +177,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
return html``; return html``;
} }
if (this._error) {
return html`<div class="errors">${this._error}</div>`;
}
return html` return html`
<ha-card .header=${this._config.title}> <ha-card .header=${this._config.title}>
<div <div
@ -153,26 +203,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
`; `;
} }
private async _getStateHistory(): Promise<void> {
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 { static get styles(): CSSResultGroup {
return css` return css`
ha-card { ha-card {