Use logbook livestream when requesting a time window that includes the future (#12744)

This commit is contained in:
J. Nick Koston 2022-05-23 12:58:50 -05:00 committed by GitHub
parent 51c5ab33f0
commit da106d278c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 196 additions and 74 deletions

View File

@ -1,6 +1,6 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain"; import { computeDomain } from "../common/entity/compute_domain";
import { BINARY_STATE_OFF, BINARY_STATE_ON } from "../common/const";
import { computeStateDisplay } from "../common/entity/compute_state_display"; import { computeStateDisplay } from "../common/entity/compute_state_display";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
@ -169,6 +169,37 @@ const getLogbookDataFromServer = (
return hass.callWS<LogbookEntry[]>(params); return hass.callWS<LogbookEntry[]>(params);
}; };
export const subscribeLogbook = (
hass: HomeAssistant,
callbackFunction: (message: LogbookEntry[]) => void,
startDate: string,
entityIds?: string[],
deviceIds?: string[]
): Promise<UnsubscribeFunc> => {
// If all specified filters are empty lists, we can return an empty list.
if (
(entityIds || deviceIds) &&
(!entityIds || entityIds.length === 0) &&
(!deviceIds || deviceIds.length === 0)
) {
return Promise.reject("No entities or devices");
}
const params: any = {
type: "logbook/event_stream",
start_time: startDate,
};
if (entityIds?.length) {
params.entity_ids = entityIds;
}
if (deviceIds?.length) {
params.device_ids = deviceIds;
}
return hass.connection.subscribeMessage<LogbookEntry[]>(
(message?) => callbackFunction(message),
params
);
};
export const clearLogbookCache = (startDate: string, endDate: string) => { export const clearLogbookCache = (startDate: string, endDate: string) => {
DATA_CACHE[`${startDate}${endDate}`] = {}; DATA_CACHE[`${startDate}${endDate}`] = {};
}; };

View File

@ -1,3 +1,4 @@
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
@ -9,12 +10,35 @@ import {
clearLogbookCache, clearLogbookCache,
getLogbookData, getLogbookData,
LogbookEntry, LogbookEntry,
subscribeLogbook,
} from "../../data/logbook"; } from "../../data/logbook";
import { loadTraceContexts, TraceContexts } from "../../data/trace"; import { loadTraceContexts, TraceContexts } from "../../data/trace";
import { fetchUsers } from "../../data/user"; import { fetchUsers } from "../../data/user";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "./ha-logbook-renderer"; import "./ha-logbook-renderer";
interface LogbookTimePeriod {
now: Date;
startTime: Date;
endTime: Date;
purgeBeforePythonTime: number | undefined;
}
const findStartOfRecentTime = (now: Date, recentTime: number) =>
new Date(now.getTime() - recentTime * 1000).getTime() / 1000;
const idsChanged = (oldIds?: string[], newIds?: string[]) => {
if (oldIds === undefined && newIds === undefined) {
return false;
}
return (
!oldIds ||
!newIds ||
oldIds.length !== newIds.length ||
!oldIds.every((val) => newIds.includes(val))
);
};
@customElement("ha-logbook") @customElement("ha-logbook")
export class HaLogbook extends LitElement { export class HaLogbook extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@ -49,19 +73,19 @@ export class HaLogbook extends LitElement {
@state() private _logbookEntries?: LogbookEntry[]; @state() private _logbookEntries?: LogbookEntry[];
@state() private _traceContexts?: TraceContexts; @state() private _traceContexts: TraceContexts = {};
@state() private _userIdToName = {}; @state() private _userIdToName = {};
@state() private _error?: string; @state() private _error?: string;
private _lastLogbookDate?: Date;
private _renderId = 1; private _renderId = 1;
private _subscribed?: Promise<UnsubscribeFunc>;
private _throttleGetLogbookEntries = throttle( private _throttleGetLogbookEntries = throttle(
() => this._getLogBookData(), () => this._getLogBookData(),
10000 1000
); );
protected render(): TemplateResult { protected render(): TemplateResult {
@ -110,10 +134,11 @@ export class HaLogbook extends LitElement {
} }
public async refresh(force = false) { public async refresh(force = false) {
if (!force && this._logbookEntries === undefined) { if (!force && (this._subscribed || this._logbookEntries === undefined)) {
return; return;
} }
this._unsubscribe();
this._throttleGetLogbookEntries.cancel(); this._throttleGetLogbookEntries.cancel();
this._updateTraceContexts.cancel(); this._updateTraceContexts.cancel();
this._updateUsers.cancel(); this._updateUsers.cancel();
@ -125,7 +150,6 @@ export class HaLogbook extends LitElement {
); );
} }
this._lastLogbookDate = undefined;
this._logbookEntries = undefined; this._logbookEntries = undefined;
this._throttleGetLogbookEntries(); this._throttleGetLogbookEntries();
} }
@ -143,12 +167,11 @@ export class HaLogbook extends LitElement {
const oldValue = changedProps.get(key) as string[] | undefined; const oldValue = changedProps.get(key) as string[] | undefined;
const curValue = this[key] as string[] | undefined; const curValue = this[key] as string[] | undefined;
if ( // If they make the filter more specific we want
!oldValue || // to change the subscription since it will reduce
!curValue || // the overhead on the backend as the event stream
oldValue.length !== curValue.length || // can be a firehose for all state events.
!oldValue.every((val) => curValue.includes(val)) if (idsChanged(oldValue, curValue)) {
) {
changed = true; changed = true;
break; break;
} }
@ -156,33 +179,6 @@ export class HaLogbook extends LitElement {
if (changed) { if (changed) {
this.refresh(true); this.refresh(true);
return;
}
if (this._filterAlwaysEmptyResults) {
return;
}
// We only need to fetch again if we track recent entries for an entity
if (
!("recent" in this.time) ||
!changedProps.has("hass") ||
!this.entityIds
) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
// Refresh data if we know the entity has changed.
if (
!oldHass ||
ensureArray(this.entityIds).some(
(entityId) => this.hass.states[entityId] !== oldHass?.states[entityId]
)
) {
// wait for commit of data (we only account for the default setting of 1 sec)
setTimeout(this._throttleGetLogbookEntries, 1000);
} }
} }
@ -198,14 +194,98 @@ export class HaLogbook extends LitElement {
); );
} }
private _unsubscribe(): void {
if (this._subscribed) {
this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
}
public connectedCallback() {
super.connectedCallback();
if (this.hasUpdated) {
this._subscribeLogbookPeriod(this._calculateLogbookPeriod());
}
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribe();
}
private _unsubscribeAndEmptyEntries() {
this._unsubscribe();
this._logbookEntries = [];
}
private _calculateLogbookPeriod() {
const now = new Date();
if ("range" in this.time) {
return <LogbookTimePeriod>{
now: now,
startTime: this.time.range[0],
endTime: this.time.range[1],
purgeBeforePythonTime: undefined,
};
}
if ("recent" in this.time) {
const purgeBeforePythonTime = findStartOfRecentTime(
now,
this.time.recent
);
return <LogbookTimePeriod>{
now: now,
startTime: new Date(purgeBeforePythonTime * 1000),
endTime: now,
purgeBeforePythonTime: findStartOfRecentTime(now, this.time.recent),
};
}
throw new Error("Unexpected time specified");
}
private _subscribeLogbookPeriod(logbookPeriod: LogbookTimePeriod) {
if (logbookPeriod.endTime < logbookPeriod.now) {
return false;
}
if (this._subscribed) {
return true;
}
this._subscribed = subscribeLogbook(
this.hass,
(newEntries?) => {
if ("recent" in this.time) {
// start time is a sliding window purge old ones
this._processNewEntries(
newEntries,
findStartOfRecentTime(new Date(), this.time.recent)
);
} else if ("range" in this.time) {
// start time is fixed, we can just append
this._processNewEntries(newEntries, undefined);
}
},
logbookPeriod.startTime.toISOString(),
ensureArray(this.entityIds),
ensureArray(this.deviceIds)
);
return true;
}
private async _getLogBookData() { private async _getLogBookData() {
this._renderId += 1; this._renderId += 1;
const renderId = this._renderId; const renderId = this._renderId;
this._error = undefined; this._error = undefined;
if (this._filterAlwaysEmptyResults) { if (this._filterAlwaysEmptyResults) {
this._logbookEntries = []; this._unsubscribeAndEmptyEntries();
this._lastLogbookDate = undefined; return;
}
const logbookPeriod = this._calculateLogbookPeriod();
if (logbookPeriod.startTime > logbookPeriod.now) {
// Time Travel not yet invented
this._unsubscribeAndEmptyEntries();
return; return;
} }
@ -214,30 +294,23 @@ export class HaLogbook extends LitElement {
this._updateTraceContexts(); this._updateTraceContexts();
} }
let startTime: Date; if (this._subscribeLogbookPeriod(logbookPeriod)) {
let endTime: Date; // We can go live
let purgeBeforePythonTime: number | undefined; return;
if ("range" in this.time) {
[startTime, endTime] = this.time.range;
} else if ("recent" in this.time) {
purgeBeforePythonTime =
new Date(new Date().getTime() - this.time.recent * 1000).getTime() /
1000;
startTime =
this._lastLogbookDate || new Date(purgeBeforePythonTime * 1000);
endTime = new Date();
} else {
throw new Error("Unexpected time specified");
} }
// We are only fetching in the past
// with a time window that does not
// extend into the future
this._unsubscribe();
let newEntries: LogbookEntry[]; let newEntries: LogbookEntry[];
try { try {
newEntries = await getLogbookData( newEntries = await getLogbookData(
this.hass, this.hass,
startTime.toISOString(), logbookPeriod.startTime.toISOString(),
endTime.toISOString(), logbookPeriod.endTime.toISOString(),
ensureArray(this.entityIds), ensureArray(this.entityIds),
ensureArray(this.deviceIds) ensureArray(this.deviceIds)
); );
@ -253,21 +326,39 @@ export class HaLogbook extends LitElement {
return; return;
} }
this._logbookEntries = [...newEntries].reverse();
}
private _nonExpiredRecords = (purgeBeforePythonTime: number | undefined) =>
!this._logbookEntries
? []
: purgeBeforePythonTime
? this._logbookEntries.filter(
(entry) => entry.when > purgeBeforePythonTime!
)
: this._logbookEntries;
private _processNewEntries = (
newEntries: LogbookEntry[],
purgeBeforePythonTime: number | undefined
) => {
// Put newest ones on top. Reverse works in-place so // Put newest ones on top. Reverse works in-place so
// make a copy first. // make a copy first.
newEntries = [...newEntries].reverse(); newEntries = [...newEntries].reverse();
if (!this._logbookEntries) {
this._logbookEntries = this._logbookEntries = newEntries;
// If we have a purgeBeforeTime, it means we're in recent-mode and fetch batches return;
purgeBeforePythonTime && this._logbookEntries
? newEntries.concat(
...this._logbookEntries.filter(
(entry) => entry.when > purgeBeforePythonTime!
)
)
: newEntries;
this._lastLogbookDate = endTime;
} }
const nonExpiredRecords = this._nonExpiredRecords(purgeBeforePythonTime);
this._logbookEntries =
newEntries[0].when >= this._logbookEntries[0].when
? // The new records are newer than the old records
// append the old records to the end of the new records
newEntries.concat(nonExpiredRecords)
: // The new records are older than the old records
// append the new records to the end of the old records
nonExpiredRecords.concat(newEntries);
};
private _updateTraceContexts = throttle(async () => { private _updateTraceContexts = throttle(async () => {
this._traceContexts = await loadTraceContexts(this.hass); this._traceContexts = await loadTraceContexts(this.hass);

View File

@ -48,10 +48,10 @@ export class HaPanelLogbook extends LitElement {
super(); super();
const start = new Date(); const start = new Date();
start.setHours(start.getHours() - 2, 0, 0, 0); start.setHours(start.getHours() - 1, 0, 0, 0);
const end = new Date(); const end = new Date();
end.setHours(end.getHours() + 1, 0, 0, 0); end.setHours(end.getHours() + 2, 0, 0, 0);
this._time = { range: [start, end] }; this._time = { range: [start, end] };
} }
@ -174,7 +174,7 @@ export class HaPanelLogbook extends LitElement {
if ( if (
!this._entityIds || !this._entityIds ||
entityIds.length !== this._entityIds.length || entityIds.length !== this._entityIds.length ||
this._entityIds.every((val, idx) => val === entityIds[idx]) !this._entityIds.every((val, idx) => val === entityIds[idx])
) { ) {
this._entityIds = entityIds; this._entityIds = entityIds;
} }