diff --git a/src/data/logbook.ts b/src/data/logbook.ts index 9e5f2ac4a6..e5e37779ee 100644 --- a/src/data/logbook.ts +++ b/src/data/logbook.ts @@ -56,17 +56,28 @@ export const getLogbookData = async ( hass: HomeAssistant, startDate: string, endDate: string, - entityId?: string + entityIds?: string[], + deviceIds?: string[] ): Promise => { const localize = await hass.loadBackendTranslation("device_class"); return addLogbookMessage( hass, localize, - await getLogbookDataCache(hass, startDate, endDate, entityId) + // bypass cache if we have a device ID + deviceIds?.length + ? await getLogbookDataFromServer( + hass, + startDate, + endDate, + entityIds, + undefined, + deviceIds + ) + : await getLogbookDataCache(hass, startDate, endDate, entityIds) ); }; -export const addLogbookMessage = ( +const addLogbookMessage = ( hass: HomeAssistant, localize: LocalizeFunc, logbookData: LogbookEntry[] @@ -86,60 +97,73 @@ export const addLogbookMessage = ( return logbookData; }; -export const getLogbookDataCache = async ( +const getLogbookDataCache = async ( hass: HomeAssistant, startDate: string, endDate: string, - entityId?: string + entityId?: string[] ) => { const ALL_ENTITIES = "*"; - if (!entityId) { - entityId = ALL_ENTITIES; - } - + const entityIdKey = entityId ? entityId.toString() : ALL_ENTITIES; const cacheKey = `${startDate}${endDate}`; if (!DATA_CACHE[cacheKey]) { DATA_CACHE[cacheKey] = {}; } - if (entityId in DATA_CACHE[cacheKey]) { - return DATA_CACHE[cacheKey][entityId]; + if (entityIdKey in DATA_CACHE[cacheKey]) { + return DATA_CACHE[cacheKey][entityIdKey]; } - if (entityId !== ALL_ENTITIES && DATA_CACHE[cacheKey][ALL_ENTITIES]) { + if (entityId && DATA_CACHE[cacheKey][ALL_ENTITIES]) { const entities = await DATA_CACHE[cacheKey][ALL_ENTITIES]; - return entities.filter((entity) => entity.entity_id === entityId); + return entities.filter( + (entity) => entity.entity_id && entityId.includes(entity.entity_id) + ); } - DATA_CACHE[cacheKey][entityId] = getLogbookDataFromServer( + DATA_CACHE[cacheKey][entityIdKey] = getLogbookDataFromServer( hass, startDate, endDate, - entityId !== ALL_ENTITIES ? entityId : undefined - ).then((entries) => entries.reverse()); - return DATA_CACHE[cacheKey][entityId]; + entityId + ); + return DATA_CACHE[cacheKey][entityIdKey]; }; -export const getLogbookDataFromServer = ( +const getLogbookDataFromServer = ( hass: HomeAssistant, startDate: string, endDate?: string, - entityId?: string, - contextId?: string -) => { - let params: any = { + entityIds?: string[], + contextId?: string, + deviceIds?: string[] +): Promise => { + // 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.resolve([]); + } + + const params: any = { type: "logbook/get_events", start_time: startDate, }; if (endDate) { - params = { ...params, end_time: endDate }; + params.end_time = endDate; } - if (entityId) { - params = { ...params, entity_ids: entityId.split(",") }; - } else if (contextId) { - params = { ...params, context_id: contextId }; + if (entityIds?.length) { + params.entity_ids = entityIds; + } + if (deviceIds?.length) { + params.device_ids = deviceIds; + } + if (contextId) { + params.context_id = contextId; } return hass.callWS(params); }; @@ -148,7 +172,7 @@ export const clearLogbookCache = (startDate: string, endDate: string) => { DATA_CACHE[`${startDate}${endDate}`] = {}; }; -export const getLogbookMessage = ( +const getLogbookMessage = ( hass: HomeAssistant, localize: LocalizeFunc, state: string, diff --git a/src/dialogs/more-info/ha-more-info-logbook.ts b/src/dialogs/more-info/ha-more-info-logbook.ts index 9757d17a8a..9a30d7929a 100644 --- a/src/dialogs/more-info/ha-more-info-logbook.ts +++ b/src/dialogs/more-info/ha-more-info-logbook.ts @@ -1,6 +1,7 @@ import { startOfYesterday } from "date-fns/esm"; import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { fireEvent } from "../../common/dom/fire_event"; import "../../panels/logbook/ha-logbook"; @@ -16,6 +17,8 @@ export class MoreInfoLogbook extends LitElement { private _time = { recent: 86400 }; + private _entityIdAsList = memoizeOne((entityId: string) => [entityId]); + protected render(): TemplateResult { if (!isComponentLoaded(this.hass, "logbook") || !this.entityId) { return html``; @@ -38,7 +41,7 @@ export class MoreInfoLogbook extends LitElement { = { name: string; @@ -52,17 +59,11 @@ declare type NameAndEntity = { }; @customElement("ha-config-area-page") -class HaConfigAreaPage extends LitElement { +class HaConfigAreaPage extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public areaId!: string; - @property() public areas!: AreaRegistryEntry[]; - - @property() public devices!: DeviceRegistryEntry[]; - - @property() public entities!: EntityRegistryEntry[]; - @property({ type: Boolean, reflect: true }) public narrow!: boolean; @property() public isWide!: boolean; @@ -71,6 +72,12 @@ class HaConfigAreaPage extends LitElement { @property() public route!: Route; + @state() public _areas!: AreaRegistryEntry[]; + + @state() public _devices!: DeviceRegistryEntry[]; + + @state() public _entities!: EntityRegistryEntry[]; + @state() private _related?: RelatedResult; private _logbookTime = { recent: 86400 }; @@ -89,7 +96,7 @@ class HaConfigAreaPage extends LitElement { registryDevices: DeviceRegistryEntry[], registryEntities: EntityRegistryEntry[] ) => { - const devices = new Map(); + const devices = new Map(); for (const device of registryDevices) { if (device.area_id === areaId) { @@ -105,7 +112,7 @@ class HaConfigAreaPage extends LitElement { if (entity.area_id === areaId) { entities.push(entity); } - } else if (devices.has(entity.device_id)) { + } else if (entity.device_id && devices.has(entity.device_id)) { indirectEntities.push(entity); } } @@ -118,6 +125,10 @@ class HaConfigAreaPage extends LitElement { } ); + private _allDeviceIds = memoizeOne((devices: DeviceRegistryEntry[]) => + devices.map((device) => device.id) + ); + private _allEntities = memoizeOne( (memberships: { entities: EntityRegistryEntry[]; @@ -140,8 +151,26 @@ class HaConfigAreaPage extends LitElement { } } + protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { + return [ + subscribeAreaRegistry(this.hass.connection, (areas) => { + this._areas = areas; + }), + subscribeDeviceRegistry(this.hass.connection, (entries) => { + this._devices = entries; + }), + subscribeEntityRegistry(this.hass.connection, (entries) => { + this._entities = entries; + }), + ]; + } + protected render(): TemplateResult { - const area = this._area(this.areaId, this.areas); + if (!this._areas || !this._devices || !this._entities) { + return html``; + } + + const area = this._area(this.areaId, this._areas); if (!area) { return html` @@ -154,8 +183,8 @@ class HaConfigAreaPage extends LitElement { const memberships = this._memberships( this.areaId, - this.devices, - this.entities + this._devices, + this._entities ); const { devices, entities } = memberships; @@ -465,6 +494,7 @@ class HaConfigAreaPage extends LitElement { .hass=${this.hass} .time=${this._logbookTime} .entityIds=${this._allEntities(memberships)} + .deviceIds=${this._allDeviceIds(memberships.devices)} virtualize narrow no-icon diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index a82b567eee..d4e6c1703d 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -1,6 +1,7 @@ import { mdiHelpCircle, mdiPlus } from "@mdi/js"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; -import { customElement, property } from "lit/decorators"; +import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import "../../../components/ha-fab"; @@ -9,12 +10,20 @@ import "../../../components/ha-svg-icon"; import { AreaRegistryEntry, createAreaRegistryEntry, + subscribeAreaRegistry, } from "../../../data/area_registry"; -import type { DeviceRegistryEntry } from "../../../data/device_registry"; -import type { EntityRegistryEntry } from "../../../data/entity_registry"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../../../data/device_registry"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../../data/entity_registry"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage"; +import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, Route } from "../../../types"; import "../ha-config-section"; import { configSections } from "../ha-panel-config"; @@ -24,7 +33,7 @@ import { } from "./show-dialog-area-registry-detail"; @customElement("ha-config-areas-dashboard") -export class HaConfigAreasDashboard extends LitElement { +export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public isWide?: boolean; @@ -33,13 +42,13 @@ export class HaConfigAreasDashboard extends LitElement { @property() public route!: Route; - @property() public areas!: AreaRegistryEntry[]; + @state() private _areas!: AreaRegistryEntry[]; - @property() public devices!: DeviceRegistryEntry[]; + @state() private _devices!: DeviceRegistryEntry[]; - @property() public entities!: EntityRegistryEntry[]; + @state() private _entities!: EntityRegistryEntry[]; - private _areas = memoizeOne( + private _processAreas = memoizeOne( ( areas: AreaRegistryEntry[], devices: DeviceRegistryEntry[], @@ -75,6 +84,20 @@ export class HaConfigAreasDashboard extends LitElement { }) ); + protected hassSubscribe(): (UnsubscribeFunc | Promise)[] { + return [ + subscribeAreaRegistry(this.hass.connection, (areas) => { + this._areas = areas; + }), + subscribeDeviceRegistry(this.hass.connection, (entries) => { + this._devices = entries; + }), + subscribeEntityRegistry(this.hass.connection, (entries) => { + this._entities = entries; + }), + ]; + } + protected render(): TemplateResult { return html` { - this._configEntries = configEntries.sort((conf1, conf2) => - stringCompare(conf1.title, conf2.title) - ); - }); - if (this._unsubs) { - return; - } - this._unsubs = [ - subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }), - subscribeDeviceRegistry(this.hass.connection, (entries) => { - this._deviceRegistryEntries = entries; - }), - subscribeEntityRegistry(this.hass.connection, (entries) => { - this._entityRegistryEntries = entries; - }), - ]; - } } declare global { diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 7f019170ab..8e989de7c3 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -134,6 +134,8 @@ export class HaConfigDevicePage extends LitElement { ) ); + private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]); + private _entityIds = memoizeOne( (entries: EntityRegistryStateEntry[]): string[] => entries.map((entry) => entry.entity_id) @@ -592,7 +594,8 @@ export class HaConfigDevicePage extends LitElement { curValue.includes(val)) + ) { + changed = true; + break; + } + } + + if (changed) { 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.entityId + !this.entityIds ) { return; } @@ -146,7 +172,7 @@ export class HaLogbook extends LitElement { // Refresh data if we know the entity has changed. if ( !oldHass || - ensureArray(this.entityId).some( + ensureArray(this.entityIds).some( (entityId) => this.hass.states[entityId] !== oldHass?.states[entityId] ) ) { @@ -155,9 +181,34 @@ export class HaLogbook extends LitElement { } } + private get _filterAlwaysEmptyResults(): boolean { + const entityIds = ensureArray(this.entityIds); + const deviceIds = ensureArray(this.deviceIds); + + // If all specified filters are empty lists, we can return an empty list. + return ( + (entityIds || deviceIds) && + (!entityIds || entityIds.length === 0) && + (!deviceIds || deviceIds.length === 0) + ); + } + private async _getLogBookData() { this._renderId += 1; const renderId = this._renderId; + this._error = undefined; + + if (this._filterAlwaysEmptyResults) { + this._logbookEntries = []; + this._lastLogbookDate = undefined; + return; + } + + this._updateUsers(); + if (this.hass.user?.is_admin) { + this._updateTraceContexts(); + } + let startTime: Date; let endTime: Date; let appendData = false; @@ -173,34 +224,21 @@ export class HaLogbook extends LitElement { endTime = new Date(); } - const entityIdFilter = this.entityId - ? ensureArray(this.entityId) - : undefined; - let newEntries: LogbookEntry[]; - if (entityIdFilter?.length === 0) { - // filtering by 0 entities, means we never can have any results - newEntries = []; - } else { - this._updateUsers(); - if (this.hass.user?.is_admin) { - this._updateTraceContexts(); - } - - try { - newEntries = await getLogbookData( - this.hass, - startTime.toISOString(), - endTime.toISOString(), - entityIdFilter ? entityIdFilter.toString() : undefined - ); - } catch (err: any) { - if (renderId === this._renderId) { - this._error = err.message; - } - return; + try { + newEntries = await getLogbookData( + this.hass, + startTime.toISOString(), + endTime.toISOString(), + ensureArray(this.entityIds), + ensureArray(this.deviceIds) + ); + } catch (err: any) { + if (renderId === this._renderId) { + this._error = err.message; } + return; } // New render happening. @@ -208,6 +246,10 @@ export class HaLogbook extends LitElement { return; } + // Put newest ones on top. Reverse works in-place so + // make a copy first. + newEntries = [...newEntries].reverse(); + this._logbookEntries = appendData && this._logbookEntries ? newEntries.concat(...this._logbookEntries) diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts index 7a64a8dbe8..65ba508e7f 100644 --- a/src/panels/logbook/ha-panel-logbook.ts +++ b/src/panels/logbook/ha-panel-logbook.ts @@ -38,7 +38,7 @@ export class HaPanelLogbook extends LitElement { @state() _time: { range: [Date, Date] }; - @state() _entityId = ""; + @state() _entityIds?: string[]; @property({ reflect: true, type: Boolean }) rtl = false; @@ -85,7 +85,7 @@ export class HaPanelLogbook extends LitElement { @@ -157,15 +157,30 @@ export class HaPanelLogbook extends LitElement { this.rtl = computeRTL(this.hass); } } - - this._applyURLParams(); } private _applyURLParams() { const searchParams = new URLSearchParams(location.search); if (searchParams.has("entity_id")) { - this._entityId = searchParams.get("entity_id") ?? ""; + const entityIdsRaw = searchParams.get("entity_id"); + + if (!entityIdsRaw) { + this._entityIds = undefined; + } else { + const entityIds = entityIdsRaw.split(",").sort(); + + // Check if different + if ( + !this._entityIds || + entityIds.length !== this._entityIds.length || + this._entityIds.every((val, idx) => val === entityIds[idx]) + ) { + this._entityIds = entityIds; + } + } + } else { + this._entityIds = undefined; } const startDateStr = searchParams.get("start_date"); @@ -199,19 +214,19 @@ export class HaPanelLogbook extends LitElement { endDate.setDate(endDate.getDate() + 1); endDate.setMilliseconds(endDate.getMilliseconds() - 1); } - this._time = { range: [startDate, endDate] }; this._updatePath({ - start_date: this._time.range[0].toISOString(), - end_date: this._time.range[1].toISOString(), + start_date: startDate.toISOString(), + end_date: endDate.toISOString(), }); } private _entityPicked(ev) { - this._entityId = ev.target.value; - this._updatePath({ entity_id: this._entityId }); + this._updatePath({ + entity_id: ev.target.value || undefined, + }); } - private _updatePath(update: Record) { + private _updatePath(update: Record) { const params = extractSearchParamsObject(); for (const [key, value] of Object.entries(update)) { if (value === undefined) { diff --git a/src/panels/lovelace/cards/hui-logbook-card.ts b/src/panels/lovelace/cards/hui-logbook-card.ts index 963644fdf8..a89cb4f048 100644 --- a/src/panels/lovelace/cards/hui-logbook-card.ts +++ b/src/panels/lovelace/cards/hui-logbook-card.ts @@ -120,7 +120,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {