From 06ee08db3649b96c83a73ecc2d3151e565672692 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 6 Dec 2022 18:30:23 +0100 Subject: [PATCH] Sort generated dashboard (#14577) --- src/common/const.ts | 8 ++ .../config/devices/ha-config-device-page.ts | 9 +- .../common/generate-lovelace-config.ts | 111 ++++++++++++------ .../card-editor/hui-dialog-suggest-card.ts | 12 +- 4 files changed, 88 insertions(+), 52 deletions(-) diff --git a/src/common/const.ts b/src/common/const.ts index 53ac073b8e..8f9902c31e 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -188,6 +188,14 @@ export const DOMAINS_WITH_CARD = [ "water_heater", ]; +export const SENSOR_ENTITIES = [ + "sensor", + "binary_sensor", + "camera", + "device_tracker", + "weather", +]; + /** Domains that render an input element instead of a text value when displayed in a row. * Those rows should then not show a cursor pointer when hovered (which would normally * be the default) unless the element itself enforces it (e.g. a button). Also those elements diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 9f184d3d12..025f048f9e 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -75,6 +75,7 @@ import { showDeviceRegistryDetailDialog, } from "./device-registry-detail/show-dialog-device-registry-detail"; import "../../../layouts/hass-subpage"; +import { SENSOR_ENTITIES } from "../../../common/const"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string | null; @@ -172,13 +173,7 @@ export class HaConfigDevicePage extends LitElement { const result = groupBy(entities, (entry) => entry.entity_category ? entry.entity_category - : [ - "sensor", - "binary_sensor", - "camera", - "device_tracker", - "weather", - ].includes(computeDomain(entry.entity_id)) + : SENSOR_ENTITIES.includes(computeDomain(entry.entity_id)) ? "sensor" : "control" ) as Record< diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 6a01fdba05..865de77944 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -1,4 +1,5 @@ import { HassEntities, HassEntity } from "home-assistant-js-websocket"; +import { SENSOR_ENTITIES } from "../../../common/const"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateName } from "../../../common/entity/compute_state_name"; @@ -23,7 +24,7 @@ import { PictureEntityCardConfig, ThermostatCardConfig, } from "../cards/types"; -import { LovelaceRowConfig } from "../entity-rows/types"; +import { EntityConfig } from "../entity-rows/types"; import { ButtonsHeaderFooterConfig } from "../header-footer/types"; const HIDE_DOMAIN = new Set([ @@ -96,14 +97,15 @@ const splitByAreaDevice = ( }; export const computeCards = ( - states: Array<[string, HassEntity?]>, + states: HassEntities, + entityIds: string[], entityCardOptions: Partial, renderFooterEntities = true ): LovelaceCardConfig[] => { const cards: LovelaceCardConfig[] = []; // For entity card - const entities: Array = []; + const entitiesConf: Array = []; const titlePrefix = entityCardOptions.title ? entityCardOptions.title.toLowerCase() @@ -111,7 +113,8 @@ export const computeCards = ( const footerEntities: ButtonsHeaderFooterConfig["entities"] = []; - for (const [entityId, stateObj] of states) { + for (const entityId of entityIds) { + const stateObj = states[entityId]; const domain = computeDomain(entityId); if (domain === "alarm_control_panel") { @@ -200,20 +203,49 @@ export const computeCards = ( } : entityId; - entities.push(entityConf); + entitiesConf.push(entityConf); } } + entitiesConf.sort((a, b) => { + const entityIdA = typeof a === "string" ? a : a.entity; + const entityIdB = typeof b === "string" ? b : b.entity; + + const categoryA = SENSOR_ENTITIES.includes(computeDomain(entityIdA)) + ? "sensor" + : "control"; + const categoryB = SENSOR_ENTITIES.includes(computeDomain(entityIdB)) + ? "sensor" + : "control"; + + if (categoryA !== categoryB) { + return categoryA === "sensor" ? 1 : -1; + } + + return stringCompare( + typeof a === "string" + ? states[a] + ? computeStateName(states[a]) + : "" + : a.name || "", + typeof b === "string" + ? states[b] + ? computeStateName(states[b]) + : "" + : b.name || "" + ); + }); + // If we ended up with footer entities but no normal entities, // render the footer entities as normal entities. - if (entities.length === 0 && footerEntities.length > 0) { - return computeCards(states, entityCardOptions, false); + if (entitiesConf.length === 0 && footerEntities.length > 0) { + return computeCards(states, entityIds, entityCardOptions, false); } - if (entities.length > 0 || footerEntities.length > 0) { + if (entitiesConf.length > 0 || footerEntities.length > 0) { const card: EntitiesCardConfig = { type: "entities", - entities, + entities: entitiesConf, ...entityCardOptions, }; if (footerEntities.length > 0) { @@ -354,15 +386,10 @@ export const generateViewConfig = ( for (const groupEntity of splitted.groups) { cards.push( - ...computeCards( - groupEntity.attributes.entity_id.map( - (entityId): [string, HassEntity] => [entityId, entities[entityId]] - ), - { - title: computeStateName(groupEntity), - show_header_toggle: groupEntity.attributes.control !== "hidden", - } - ) + ...computeCards(entities, groupEntity.attributes.entity_id, { + title: computeStateName(groupEntity), + show_header_toggle: groupEntity.attributes.control !== "hidden", + }) ); } @@ -398,17 +425,13 @@ export const generateViewConfig = ( .forEach((domain) => { cards.push( ...computeCards( - ungroupedEntitites[domain] - .sort((a, b) => - stringCompare( - computeStateName(entities[a]), - computeStateName(entities[b]) - ) + entities, + ungroupedEntitites[domain].sort((a, b) => + stringCompare( + computeStateName(entities[a]), + computeStateName(entities[b]) ) - .map((entityId): [string, HassEntity] => [ - entityId, - entities[entityId], - ]), + ), { title: domainTranslations[domain], } @@ -466,28 +489,35 @@ export const generateDefaultViewConfig = ( groupOrders ); - const splittedCards: LovelaceCardConfig[] = []; + const areaCards: LovelaceCardConfig[] = []; for (const [areaId, areaEntities] of Object.entries( splittedByAreaDevice.areasWithEntities )) { const area = areaEntries[areaId]; - splittedCards.push( + areaCards.push( ...computeCards( - areaEntities.map((entity) => [entity.entity_id, entity]), + entities, + areaEntities.map((entity) => entity.entity_id), { title: area.name, } ) ); } + + areaCards.sort((a, b) => stringCompare(a.title || "", b.title || "")); + + const deviceCards: LovelaceCardConfig[] = []; + for (const [deviceId, deviceEntities] of Object.entries( splittedByAreaDevice.devicesWithEntities )) { const device = deviceEntries[deviceId]; - splittedCards.push( + deviceCards.push( ...computeCards( - deviceEntities.map((entity) => [entity.entity_id, entity]), + entities, + deviceEntities.map((entity) => entity.entity_id), { title: device.name_by_user || @@ -503,6 +533,11 @@ export const generateDefaultViewConfig = ( ) ); } + + deviceCards.sort((a, b) => stringCompare(a.title || "", b.title || "")); + + let energyCard: LovelaceCardConfig | undefined; + if (energyPrefs) { // Distribution card requires the grid to be configured const grid = energyPrefs.energy_sources.find( @@ -510,17 +545,21 @@ export const generateDefaultViewConfig = ( ) as GridSourceTypeEnergyPreference | undefined; if (grid && grid.flow_from.length > 0) { - splittedCards.push({ + energyCard = { title: localize( "ui.panel.lovelace.cards.energy.energy_distribution.title_today" ), type: "energy-distribution", link_dashboard: true, - }); + }; } } - config.cards!.unshift(...splittedCards); + config.cards!.unshift( + ...areaCards, + ...(energyCard ? [energyCard] : []), + ...deviceCards + ); return config; }; diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts index 2ac61bee0f..71851cf06b 100755 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts @@ -30,15 +30,9 @@ export class HuiDialogSuggestCard extends LitElement { this._params = params; this._cardConfig = params.cardConfig || - computeCards( - params.entities.map((entityId) => [ - entityId, - this.hass.states[entityId], - ]), - { - title: params.cardTitle, - } - ); + computeCards(this.hass.states, params.entities, { + title: params.cardTitle, + }); if (!Object.isFrozen(this._cardConfig)) { this._cardConfig = deepFreeze(this._cardConfig); }