From 8b811743950747b4834c28bc083f79a4cbf69303 Mon Sep 17 00:00:00 2001 From: Zack Arnett Date: Fri, 19 Nov 2021 13:11:40 -0600 Subject: [PATCH] Updates to Strategy --- .../common/generate-lovelace-config.ts | 120 +++++++++++++++++- .../strategies/area-overview-strategy.ts | 6 +- .../lovelace/strategies/area-strategy.ts | 100 +++++++++++++++ .../lovelace/strategies/get-strategy.ts | 1 + .../strategies/original-states-strategy.ts | 7 +- 5 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 src/panels/lovelace/strategies/area-strategy.ts diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 9627a0364e..3ef794fccf 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -86,10 +86,22 @@ const splitByAreas = ( export const computeCards = ( states: Array<[string, HassEntity?]>, - entityCardOptions: Partial + entityCardOptions: Partial, + dedicated?: boolean ): LovelaceCardConfig[] => { const cards: LovelaceCardConfig[] = []; - + const sensorGrid: { type: string; cards: LovelaceCardConfig[] } = { + type: "grid", + columns: 2, + square: false, + cards: [], + }; + const toggleGrid: { type: string; cards: LovelaceCardConfig[] } = { + type: "grid", + columns: 2, + square: false, + cards: [], + }; // For entity card const entities: Array = []; @@ -148,6 +160,39 @@ export const computeCards = ( stateObj?.attributes.device_class === SENSOR_DEVICE_CLASS_BATTERY ) { // Do nothing. + } else if (dedicated) { + if (domain === "binary_sensor") { + const cardConfig = { + type: "entity", + entity: entityId, + }; + sensorGrid.cards.push(cardConfig); + } else if (domain === "sensor") { + const cardConfig = { + type: "sensor", + entity: entityId, + graph: "line", + }; + sensorGrid.cards.push(cardConfig); + } else if (domain === "switch") { + const cardConfig = { + type: "button", + entity: entityId, + }; + toggleGrid.cards.push(cardConfig); + } else if (domain === "fan") { + const cardConfig = { + type: "button", + entity: entityId, + }; + toggleGrid.cards.push(cardConfig); + } else if (domain === "light") { + const cardConfig = { + type: "light", + entity: entityId, + }; + toggleGrid.cards.push(cardConfig); + } } else { let name: string | undefined; const entityConf = @@ -176,6 +221,14 @@ export const computeCards = ( }); } + if (sensorGrid.cards.length > 0) { + cards.push(sensorGrid); + } + + if (toggleGrid.cards.length > 0) { + cards.push(toggleGrid); + } + return cards; }; @@ -345,8 +398,7 @@ export const generateDefaultViewConfig = ( entityEntries: EntityRegistryEntry[], entities: HassEntities, localize: LocalizeFunc, - energyPrefs?: EnergyPreferences, - areaOnly?: boolean + energyPrefs?: EnergyPreferences ): LovelaceViewConfig => { const states = computeDefaultViewStates(entities, entityEntries); const path = "default_view"; @@ -374,7 +426,7 @@ export const generateDefaultViewConfig = ( path, title, icon, - areaOnly ? {} : splittedByAreas.otherEntities, + splittedByAreas.otherEntities, groupOrders ); @@ -391,7 +443,7 @@ export const generateDefaultViewConfig = ( ); }); - if (energyPrefs && !areaOnly) { + if (energyPrefs) { // Distribution card requires the grid to be configured const grid = energyPrefs.energy_sources.find( (source) => source.type === "grid" @@ -410,3 +462,59 @@ export const generateDefaultViewConfig = ( return config; }; + +export const generateAreaViewConfig = ( + area: AreaRegistryEntry, + deviceEntries: DeviceRegistryEntry[], + entityEntries: EntityRegistryEntry[], + entities: HassEntities, + localize: LocalizeFunc +): LovelaceViewConfig => { + const states = computeDefaultViewStates(entities, entityEntries); + 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( + [area], + deviceEntries, + entityEntries, + states + ); + + const config = generateViewConfig( + localize, + path, + title, + icon, + {}, + groupOrders + ); + + const areaCards: LovelaceCardConfig[] = []; + + splittedByAreas.areasWithEntities.forEach(([a, areaEntities]) => { + areaCards.push( + ...computeCards( + areaEntities.map((entity) => [entity.entity_id, entity]), + { + title: a.name, + }, + true + ) + ); + }); + + config.cards!.unshift(...areaCards); + + return config; +}; diff --git a/src/panels/lovelace/strategies/area-overview-strategy.ts b/src/panels/lovelace/strategies/area-overview-strategy.ts index baccfc2fb1..e4022d1333 100644 --- a/src/panels/lovelace/strategies/area-overview-strategy.ts +++ b/src/panels/lovelace/strategies/area-overview-strategy.ts @@ -46,8 +46,6 @@ export class AreaOverviewStrategy { view.cards?.push({ type: "area", area: area.area_id, - image: - "https://www.boardandvellum.com/wp-content/uploads/2019/09/16x9-private_offices_vs_open_office_concepts-1242x699.jpg", }); }); @@ -63,8 +61,8 @@ export class AreaOverviewStrategy { const areaViews = areaEntries.map((area) => ({ strategy: { - type: "original-states", - options: { areaId: area.area_id }, + type: "area", + options: { area_id: area.area_id }, }, title: area.name, })); diff --git a/src/panels/lovelace/strategies/area-strategy.ts b/src/panels/lovelace/strategies/area-strategy.ts new file mode 100644 index 0000000000..6fb38ddf4f --- /dev/null +++ b/src/panels/lovelace/strategies/area-strategy.ts @@ -0,0 +1,100 @@ +import { STATE_NOT_RUNNING } from "home-assistant-js-websocket"; +import { subscribeOne } from "../../../common/util/subscribe-one"; +import { + AreaRegistryEntry, + subscribeAreaRegistry, +} from "../../../data/area_registry"; +import { + DeviceRegistryEntry, + subscribeDeviceRegistry, +} from "../../../data/device_registry"; +import { subscribeEntityRegistry } from "../../../data/entity_registry"; +import { generateAreaViewConfig } from "../common/generate-lovelace-config"; +import { + LovelaceDashboardStrategy, + LovelaceViewStrategy, +} from "./get-strategy"; + +let subscribedRegistries = false; + +export class AreaStrategy { + static async generateView( + info: Parameters[0] + ): ReturnType { + const hass = info.hass; + const areaId = info.view.strategy?.options?.area_id; + + if (hass.config.state === STATE_NOT_RUNNING) { + return { + cards: [{ type: "starting" }], + }; + } + + if (hass.config.safe_mode) { + return { + cards: [{ type: "safe-mode" }], + }; + } + + let areaEntry: AreaRegistryEntry | undefined; + let deviceEntries: DeviceRegistryEntry[] | undefined; + + // We leave this here so we always have the freshest data. + if (!subscribedRegistries) { + subscribedRegistries = true; + subscribeAreaRegistry(hass.connection, (areas) => { + areaEntry = areas.find((area) => area.area_id === areaId); + }); + subscribeDeviceRegistry(hass.connection, (devices) => { + deviceEntries = devices.filter((device) => device.area_id === areaId); + }); + subscribeEntityRegistry(hass.connection, () => undefined); + } + + // eslint-disable-next-line unused-imports/no-unused-vars + const [localize, entityEntries] = await Promise.all([ + hass.loadBackendTranslation("title"), + subscribeOne(hass.connection, subscribeEntityRegistry), + subscribeOne(hass.connection, subscribeAreaRegistry), + subscribeOne(hass.connection, subscribeDeviceRegistry), + ]); + + if (!areaEntry) { + return { + cards: [{ type: "error" }], + }; + } + + // User can override default view. If they didn't, we will add one + // that contains all entities. + const view = generateAreaViewConfig( + areaEntry, + deviceEntries || [], + entityEntries, + hass.states, + localize + ); + + // User has no entities + if (view.cards!.length === 0) { + view.cards!.push({ + type: "empty-state", + }); + } + + return view; + } + + static async generateDashboard( + info: Parameters[0] + ): ReturnType { + return { + title: info.hass.config.location_name, + views: [ + { + strategy: { type: "area" }, + }, + ], + }; + } +} diff --git a/src/panels/lovelace/strategies/get-strategy.ts b/src/panels/lovelace/strategies/get-strategy.ts index 6377b4382f..86f360da0a 100644 --- a/src/panels/lovelace/strategies/get-strategy.ts +++ b/src/panels/lovelace/strategies/get-strategy.ts @@ -31,6 +31,7 @@ const strategies: Record< (await import("../../energy/strategies/energy-strategy")).EnergyStrategy, "area-overview": async () => (await import("./area-overview-strategy")).AreaOverviewStrategy, + area: async () => (await import("./area-strategy")).AreaStrategy, }; const getLovelaceStrategy = async < diff --git a/src/panels/lovelace/strategies/original-states-strategy.ts b/src/panels/lovelace/strategies/original-states-strategy.ts index 3b7aaa731b..a083edd03a 100644 --- a/src/panels/lovelace/strategies/original-states-strategy.ts +++ b/src/panels/lovelace/strategies/original-states-strategy.ts @@ -69,15 +69,12 @@ export class OriginalStatesStrategy { const view = generateDefaultViewConfig( !areaId ? areaEntries - : areaEntries.filter( - (area) => area.area_id === info.view.strategy?.options?.area - ), + : areaEntries.filter((area) => area.area_id === areaId), deviceEntries, entityEntries, hass.states, localize, - energyPrefs, - Boolean(info.view.strategy?.options?.area) + energyPrefs ); // Add map of geo locations to default view if loaded