diff --git a/package.json b/package.json index 3cf476e4b9..edaa0308dc 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "es6-object-assign": "^1.1.0", "fecha": "^3.0.2", "hls.js": "^0.12.4", - "home-assistant-js-websocket": "^4.1.1", + "home-assistant-js-websocket": "^4.1.2", "intl-messageformat": "^2.2.0", "jquery": "^3.3.1", "js-yaml": "^3.13.0", diff --git a/src/common/util/subscribe-one.ts b/src/common/util/subscribe-one.ts new file mode 100644 index 0000000000..f7fee163a7 --- /dev/null +++ b/src/common/util/subscribe-one.ts @@ -0,0 +1,16 @@ +import { HomeAssistant } from "../../types"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; + +export const subscribeOne = async ( + hass: HomeAssistant, + subscribe: ( + hass: HomeAssistant, + onChange: (items: T) => void + ) => UnsubscribeFunc +) => + new Promise((resolve) => { + const unsub = subscribe(hass, (items) => { + unsub(); + resolve(items); + }); + }); diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index c0b115e523..666f9fe3ff 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -17,6 +17,19 @@ import computeDomain from "../../../common/entity/compute_domain"; import { EntityRowConfig, WeblinkConfig } from "../entity-rows/types"; import { LocalizeFunc } from "../../../common/translations/localize"; import { EntitiesCardConfig } from "../cards/types"; +import { + subscribeAreaRegistry, + AreaRegistryEntry, +} from "../../../data/area_registry"; +import { subscribeOne } from "../../../common/util/subscribe-one"; +import { + subscribeDeviceRegistry, + DeviceRegistryEntry, +} from "../../../data/device_registry"; +import { + subscribeEntityRegistry, + EntityRegistryEntry, +} from "../../../data/entity_registry"; const DEFAULT_VIEW_ENTITY_ID = "group.default_view"; const DOMAINS_BADGES = [ @@ -34,6 +47,55 @@ const HIDE_DOMAIN = new Set([ "geo_location", ]); +interface Registries { + areas: AreaRegistryEntry[]; + devices: DeviceRegistryEntry[]; + entities: EntityRegistryEntry[]; +} + +let subscribedRegistries = false; + +interface SplittedByAreas { + areasWithEntities: Array<[AreaRegistryEntry, HassEntity[]]>; + otherEntities: HassEntities; +} + +const splitByAreas = ( + registries: Registries, + entities: HassEntities +): SplittedByAreas => { + const allEntities = { ...entities }; + const areasWithEntities: SplittedByAreas["areasWithEntities"] = []; + + for (const area of registries.areas) { + const areaEntities: HassEntity[] = []; + const areaDevices = new Set( + registries.devices + .filter((device) => device.area_id === area.area_id) + .map((device) => device.id) + ); + for (const entity of registries.entities) { + if ( + areaDevices.has( + // @ts-ignore + entity.device_id + ) && + entity.entity_id in allEntities + ) { + areaEntities.push(allEntities[entity.entity_id]); + delete allEntities[entity.entity_id]; + } + } + if (areaEntities.length > 0) { + areasWithEntities.push([area, areaEntities]); + } + } + return { + areasWithEntities, + otherEntities: allEntities, + }; +}; + const computeCards = ( states: Array<[string, HassEntity]>, entityCardOptions: Partial @@ -124,6 +186,51 @@ const computeDefaultViewStates = (hass: HomeAssistant): HassEntities => { return states; }; +const generateDefaultViewConfig = ( + hass: HomeAssistant, + registries: Registries +): LovelaceViewConfig => { + const states = computeDefaultViewStates(hass); + const path = "default_view"; + const title = "Home"; + const icon = undefined; + + // In the case of a default view, we want to use the group order attribute + const groupOrders = {}; + Object.keys(states).forEach((entityId) => { + const stateObj = states[entityId]; + if (stateObj.attributes.order) { + groupOrders[entityId] = stateObj.attributes.order; + } + }); + + const splittedByAreas = splitByAreas(registries, states); + + const config = generateViewConfig( + hass.localize, + path, + title, + icon, + splittedByAreas.otherEntities, + groupOrders + ); + + const areaCards: LovelaceCardConfig[] = []; + + splittedByAreas.areasWithEntities.forEach(([area, entities]) => { + areaCards.push( + ...computeCards(entities.map((entity) => [entity.entity_id, entity]), { + title: area.name, + show_header_toggle: true, + }) + ); + }); + + config.cards!.unshift(...areaCards); + + return config; +}; + const generateViewConfig = ( localize: LocalizeFunc, path: string, @@ -208,10 +315,10 @@ const generateViewConfig = ( return view; }; -export const generateLovelaceConfig = ( +export const generateLovelaceConfig = async ( hass: HomeAssistant, localize: LocalizeFunc -): LovelaceConfig => { +): Promise => { const viewEntities = extractViews(hass.states); const views = viewEntities.map((viewEntity: GroupEntity) => { @@ -241,27 +348,23 @@ export const generateLovelaceConfig = ( viewEntities.length === 0 || viewEntities[0].entity_id !== DEFAULT_VIEW_ENTITY_ID ) { - const states = computeDefaultViewStates(hass); + // We want to keep the registry subscriptions alive after generating the UI + // so that we don't serve up stale data after changing areas. + if (!subscribedRegistries) { + subscribedRegistries = true; + subscribeAreaRegistry(hass, () => undefined); + subscribeDeviceRegistry(hass, () => undefined); + subscribeEntityRegistry(hass, () => undefined); + } - // In the case of a default view, we want to use the group order attribute - const groupOrders = {}; - Object.keys(states).forEach((entityId) => { - const stateObj = states[entityId]; - if (stateObj.attributes.order) { - groupOrders[entityId] = stateObj.attributes.order; - } - }); + const [areas, devices, entities] = await Promise.all([ + subscribeOne(hass, subscribeAreaRegistry), + subscribeOne(hass, subscribeDeviceRegistry), + subscribeOne(hass, subscribeEntityRegistry), + ]); + const registries = { areas, devices, entities }; - views.unshift( - generateViewConfig( - localize, - "default_view", - "Home", - undefined, - states, - groupOrders - ) - ); + views.unshift(generateDefaultViewConfig(hass, registries)); // Add map of geo locations to default view if loaded if (hass.config.components.includes("geo_location")) { diff --git a/src/panels/lovelace/ha-panel-lovelace.ts b/src/panels/lovelace/ha-panel-lovelace.ts index b3b4168d11..98a97411c8 100644 --- a/src/panels/lovelace/ha-panel-lovelace.ts +++ b/src/panels/lovelace/ha-panel-lovelace.ts @@ -171,7 +171,7 @@ class LovelacePanel extends LitElement { this._errorMsg = err.message; return; } - conf = generateLovelaceConfig(this.hass!, this.hass!.localize); + conf = await generateLovelaceConfig(this.hass!, this.hass!.localize); confMode = "generated"; } diff --git a/yarn.lock b/yarn.lock index 593634e982..42d45f0325 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7237,10 +7237,10 @@ hoek@6.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== -home-assistant-js-websocket@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.1.tgz#b85152c223a20bfe8827b817b927fd97cc021157" - integrity sha512-hNk8bj9JObd3NpgQ1+KtQCbSoz/TWockC8T/L8KvsPrDtkl1oQddajirumaMDgrJg/su4QsxFNUcDPGJyJ05UA== +home-assistant-js-websocket@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-4.1.2.tgz#dbcdb4b67df8d189d29bbf5603771d5bc80ef031" + integrity sha512-/I0m6FTDEq3LkzFc4tmgHJHTj9gWA6Wn/fgaa1ghIJJY0Yqb3x6whovN5pRNFsl6bnKzOCR+nmJ2ruVTBa5mVQ== homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1: version "1.0.3"