From c119163422d37f1ffdb511cb2ddc1ada51beb688 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 28 Nov 2022 16:56:35 +0100 Subject: [PATCH] Add grouping by device to generated dashboard config (#14398) --- cast/src/launcher/layout/hc-cast.ts | 2 +- .../common/generate-lovelace-config.ts | 144 +++++++++++------- .../strategies/original-states-strategy.ts | 38 ++--- 3 files changed, 99 insertions(+), 85 deletions(-) diff --git a/cast/src/launcher/layout/hc-cast.ts b/cast/src/launcher/layout/hc-cast.ts index 9c8debb48b..2b58d6347d 100644 --- a/cast/src/launcher/layout/hc-cast.ts +++ b/cast/src/launcher/layout/hc-cast.ts @@ -88,7 +88,7 @@ class HcCast extends LitElement { > ${(this.lovelaceConfig ? this.lovelaceConfig.views - : [generateDefaultViewConfig([], [], [], {}, () => "")] + : [generateDefaultViewConfig({}, {}, {}, {}, () => "")] ).map( (view, idx) => html` ; +interface SplittedByAreaDevice { + areasWithEntities: { [areaId: string]: HassEntity[] }; + devicesWithEntities: { [deviceId: string]: HassEntity[] }; otherEntities: HassEntities; } -const splitByAreas = ( - areaEntries: AreaRegistryEntry[], - deviceEntries: DeviceRegistryEntry[], - entityEntries: EntityRegistryEntry[], +const splitByAreaDevice = ( + deviceEntries: HomeAssistant["devices"], + entityEntries: HomeAssistant["entities"], entities: HassEntities -): SplittedByAreas => { +): SplittedByAreaDevice => { const allEntities = { ...entities }; - const areasWithEntities: SplittedByAreas["areasWithEntities"] = []; + const areasWithEntities: SplittedByAreaDevice["areasWithEntities"] = {}; + const devicesWithEntities: SplittedByAreaDevice["devicesWithEntities"] = {}; - for (const area of areaEntries) { - const areaEntities: HassEntity[] = []; - const areaDevices = new Set( - deviceEntries - .filter((device) => device.area_id === area.area_id) - .map((device) => device.id) - ); - for (const entity of entityEntries) { - if ( - ((areaDevices.has( - // @ts-ignore - entity.device_id - ) && - !entity.area_id) || - entity.area_id === area.area_id) && - entity.entity_id in allEntities - ) { - areaEntities.push(allEntities[entity.entity_id]); - delete allEntities[entity.entity_id]; + const areaDevices = new Set( + Object.values(deviceEntries) + .filter((device) => device.area_id) + .map((device) => device.id) + ); + for (const entity of Object.values(entityEntries)) { + if ( + (entity.area_id || + (entity.device_id && areaDevices.has(entity.device_id))) && + entity.entity_id in allEntities + ) { + const areaId = + entity.area_id || deviceEntries[entity.device_id!].area_id!; + if (!(areaId in areasWithEntities)) { + areasWithEntities[areaId] = []; } + areasWithEntities[areaId].push(allEntities[entity.entity_id]); + delete allEntities[entity.entity_id]; + } else if (entity.device_id && entity.entity_id in allEntities) { + if (!(entity.device_id in devicesWithEntities)) { + devicesWithEntities[entity.device_id] = []; + } + devicesWithEntities[entity.device_id].push(allEntities[entity.entity_id]); + delete allEntities[entity.entity_id]; } - if (areaEntities.length > 0) { - areasWithEntities.push([area, areaEntities]); + } + for (const [deviceId, deviceEntities] of Object.entries( + devicesWithEntities + )) { + if (deviceEntities.length === 1) { + allEntities[deviceEntities[0].entity_id] = deviceEntities[0]; + delete devicesWithEntities[deviceId]; } } return { areasWithEntities, + devicesWithEntities, otherEntities: allEntities, }; }; @@ -232,11 +241,11 @@ export const computeCards = ( const computeDefaultViewStates = ( entities: HassEntities, - entityEntries: EntityRegistryEntry[] + entityEntries: HomeAssistant["entities"] ): HassEntities => { const states = {}; const hiddenEntities = new Set( - entityEntries + Object.values(entityEntries) .filter( (entry) => entry.entity_category || @@ -246,7 +255,7 @@ const computeDefaultViewStates = ( .map((entry) => entry.entity_id) ); - Object.keys(entities).forEach((entityId) => { + for (const entityId of Object.keys(entities)) { const stateObj = entities[entityId]; if ( !HIDE_DOMAIN.has(computeStateDomain(stateObj)) && @@ -254,7 +263,7 @@ const computeDefaultViewStates = ( ) { states[entityId] = entities[entityId]; } - }); + } return states; }; @@ -274,7 +283,7 @@ export const generateViewConfig = ( const ungroupedEntitites: { [domain: string]: string[] } = {}; // Organize ungrouped entities in ungrouped things - Object.keys(splitted.ungrouped).forEach((entityId) => { + for (const entityId of Object.keys(splitted.ungrouped)) { const state = splitted.ungrouped[entityId]; const domain = computeStateDomain(state); @@ -283,7 +292,7 @@ export const generateViewConfig = ( } ungroupedEntitites[domain].push(state.entity_id); - }); + } const cards: LovelaceCardConfig[] = []; @@ -343,7 +352,7 @@ export const generateViewConfig = ( delete ungroupedEntitites.person; } - splitted.groups.forEach((groupEntity) => { + for (const groupEntity of splitted.groups) { cards.push( ...computeCards( groupEntity.attributes.entity_id.map( @@ -355,7 +364,7 @@ export const generateViewConfig = ( } ) ); - }); + } // Group helper entities in a single card const helperEntities: string[] = []; @@ -421,9 +430,9 @@ export const generateViewConfig = ( }; export const generateDefaultViewConfig = ( - areaEntries: AreaRegistryEntry[], - deviceEntries: DeviceRegistryEntry[], - entityEntries: EntityRegistryEntry[], + areaEntries: HomeAssistant["areas"], + deviceEntries: HomeAssistant["devices"], + entityEntries: HomeAssistant["entities"], entities: HassEntities, localize: LocalizeFunc, energyPrefs?: EnergyPreferences @@ -435,15 +444,14 @@ export const generateDefaultViewConfig = ( // In the case of a default view, we want to use the group order attribute const groupOrders = {}; - Object.keys(states).forEach((entityId) => { + for (const entityId of Object.keys(states)) { const stateObj = states[entityId]; if (stateObj.attributes.order) { groupOrders[entityId] = stateObj.attributes.order; } - }); + } - const splittedByAreas = splitByAreas( - areaEntries, + const splittedByAreaDevice = splitByAreaDevice( deviceEntries, entityEntries, states @@ -454,14 +462,17 @@ export const generateDefaultViewConfig = ( path, title, icon, - splittedByAreas.otherEntities, + splittedByAreaDevice.otherEntities, groupOrders ); - const areaCards: LovelaceCardConfig[] = []; + const splittedCards: LovelaceCardConfig[] = []; - splittedByAreas.areasWithEntities.forEach(([area, areaEntities]) => { - areaCards.push( + for (const [areaId, areaEntities] of Object.entries( + splittedByAreaDevice.areasWithEntities + )) { + const area = areaEntries[areaId]; + splittedCards.push( ...computeCards( areaEntities.map((entity) => [entity.entity_id, entity]), { @@ -469,8 +480,29 @@ export const generateDefaultViewConfig = ( } ) ); - }); - + } + for (const [deviceId, deviceEntities] of Object.entries( + splittedByAreaDevice.devicesWithEntities + )) { + const device = deviceEntries[deviceId]; + splittedCards.push( + ...computeCards( + deviceEntities.map((entity) => [entity.entity_id, entity]), + { + title: + device.name_by_user || + device.name || + localize( + "ui.panel.config.devices.unnamed_device", + "type", + localize( + `ui.panel.config.devices.type.${device.entry_type || "device"}` + ) + ), + } + ) + ); + } if (energyPrefs) { // Distribution card requires the grid to be configured const grid = energyPrefs.energy_sources.find( @@ -478,7 +510,7 @@ export const generateDefaultViewConfig = ( ) as GridSourceTypeEnergyPreference | undefined; if (grid && grid.flow_from.length > 0) { - areaCards.push({ + splittedCards.push({ title: localize( "ui.panel.lovelace.cards.energy.energy_distribution.title_today" ), @@ -488,7 +520,7 @@ export const generateDefaultViewConfig = ( } } - config.cards!.unshift(...areaCards); + config.cards!.unshift(...splittedCards); return config; }; diff --git a/src/panels/lovelace/strategies/original-states-strategy.ts b/src/panels/lovelace/strategies/original-states-strategy.ts index d94b1ede87..7c538d5fc1 100644 --- a/src/panels/lovelace/strategies/original-states-strategy.ts +++ b/src/panels/lovelace/strategies/original-states-strategy.ts @@ -1,18 +1,12 @@ import { STATE_NOT_RUNNING } from "home-assistant-js-websocket"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; -import { subscribeOne } from "../../../common/util/subscribe-one"; -import { subscribeAreaRegistry } from "../../../data/area_registry"; -import { subscribeDeviceRegistry } from "../../../data/device_registry"; import { getEnergyPreferences } from "../../../data/energy"; -import { subscribeEntityRegistry } from "../../../data/entity_registry"; import { generateDefaultViewConfig } from "../common/generate-lovelace-config"; import { LovelaceDashboardStrategy, LovelaceViewStrategy, } from "./get-strategy"; -let subscribedRegistries = false; - export class OriginalStatesStrategy { static async generateView( info: Parameters[0] @@ -31,32 +25,20 @@ export class OriginalStatesStrategy { }; } - // We leave this here so we always have the freshest data. - if (!subscribedRegistries) { - subscribedRegistries = true; - subscribeAreaRegistry(hass.connection, () => undefined); - subscribeDeviceRegistry(hass.connection, () => undefined); - subscribeEntityRegistry(hass.connection, () => undefined); - } - - const [areaEntries, deviceEntries, entityEntries, localize, energyPrefs] = - await Promise.all([ - subscribeOne(hass.connection, subscribeAreaRegistry), - subscribeOne(hass.connection, subscribeDeviceRegistry), - subscribeOne(hass.connection, subscribeEntityRegistry), - hass.loadBackendTranslation("title"), - isComponentLoaded(hass, "energy") - ? // It raises if not configured, just swallow that. - getEnergyPreferences(hass).catch(() => undefined) - : undefined, - ]); + const [localize, energyPrefs] = await Promise.all([ + hass.loadBackendTranslation("title"), + isComponentLoaded(hass, "energy") + ? // It raises if not configured, just swallow that. + getEnergyPreferences(hass).catch(() => undefined) + : undefined, + ]); // User can override default view. If they didn't, we will add one // that contains all entities. const view = generateDefaultViewConfig( - areaEntries, - deviceEntries, - entityEntries, + hass.areas, + hass.devices, + hass.entities, hass.states, localize, energyPrefs