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

View File

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