mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 03:36:44 +00:00
Use logbook livestream when requesting a time window that includes the future (#12744)
This commit is contained in:
parent
51c5ab33f0
commit
da106d278c
@ -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}`] = {};
|
||||||
};
|
};
|
||||||
|
@ -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 = newEntries;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nonExpiredRecords = this._nonExpiredRecords(purgeBeforePythonTime);
|
||||||
this._logbookEntries =
|
this._logbookEntries =
|
||||||
// If we have a purgeBeforeTime, it means we're in recent-mode and fetch batches
|
newEntries[0].when >= this._logbookEntries[0].when
|
||||||
purgeBeforePythonTime && this._logbookEntries
|
? // The new records are newer than the old records
|
||||||
? newEntries.concat(
|
// append the old records to the end of the new records
|
||||||
...this._logbookEntries.filter(
|
newEntries.concat(nonExpiredRecords)
|
||||||
(entry) => entry.when > purgeBeforePythonTime!
|
: // The new records are older than the old records
|
||||||
)
|
// append the new records to the end of the old records
|
||||||
)
|
nonExpiredRecords.concat(newEntries);
|
||||||
: newEntries;
|
};
|
||||||
this._lastLogbookDate = endTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _updateTraceContexts = throttle(async () => {
|
private _updateTraceContexts = throttle(async () => {
|
||||||
this._traceContexts = await loadTraceContexts(this.hass);
|
this._traceContexts = await loadTraceContexts(this.hass);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user