From a6c9702ab21da49c17cbb930e5856f6d5602dc58 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 10 Apr 2025 14:43:08 +0200 Subject: [PATCH] Update entity naming in entities config page (#24966) Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com> --- src/common/entity/compute_device_name.ts | 12 +++ src/common/string/get_duplicates.ts | 14 +++ .../devices/ha-config-devices-dashboard.ts | 2 +- .../config/entities/ha-config-entities.ts | 98 +++++++++++++------ src/translations/en.json | 5 +- 5 files changed, 96 insertions(+), 35 deletions(-) create mode 100644 src/common/string/get_duplicates.ts diff --git a/src/common/entity/compute_device_name.ts b/src/common/entity/compute_device_name.ts index b0f1cc7d14..f107f55096 100644 --- a/src/common/entity/compute_device_name.ts +++ b/src/common/entity/compute_device_name.ts @@ -1,3 +1,4 @@ +import memoizeOne from "memoize-one"; import type { DeviceRegistryEntry } from "../../data/device_registry"; import type { EntityRegistryDisplayEntry, @@ -5,6 +6,7 @@ import type { } from "../../data/entity_registry"; import type { HomeAssistant } from "../../types"; import { computeStateName } from "./compute_state_name"; +import { getDuplicates } from "../string/get_duplicates"; export const computeDeviceName = ( device: DeviceRegistryEntry @@ -36,3 +38,13 @@ export const fallbackDeviceName = ( } return undefined; }; + +export const getDuplicatedDeviceNames = memoizeOne( + (devices: HomeAssistant["devices"]): Set => { + const names = Object.values(devices) + .map((device) => computeDeviceName(device)) + .filter((name): name is string => name !== undefined); + + return getDuplicates(names); + } +); diff --git a/src/common/string/get_duplicates.ts b/src/common/string/get_duplicates.ts new file mode 100644 index 0000000000..ef9a0e2371 --- /dev/null +++ b/src/common/string/get_duplicates.ts @@ -0,0 +1,14 @@ +export function getDuplicates(array: string[]): Set { + const duplicates = new Set(); + const seen = new Set(); + + for (const item of array) { + if (seen.has(item)) { + duplicates.add(item); + } else { + seen.add(item); + } + } + + return duplicates; +} diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index 9e276e3978..91d793f2b9 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -497,7 +497,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { : "", }, name: { - title: localize("ui.panel.config.devices.data_table.name"), + title: localize("ui.panel.config.devices.data_table.device"), main: true, sortable: true, filterable: true, diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts index 4e4e6d357e..94e3fdf13e 100644 --- a/src/panels/config/entities/ha-config-entities.ts +++ b/src/panels/config/entities/ha-config-entities.ts @@ -25,15 +25,18 @@ import { computeCssColor } from "../../../common/color/compute-color"; import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time"; import { storage } from "../../../common/decorators/storage"; import type { HASSDomEvent } from "../../../common/dom/fire_event"; -import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeAreaName } from "../../../common/entity/compute_area_name"; import { - isDeletableEntity, - deleteEntity, -} from "../../../common/entity/delete_entity"; -import type { Helper } from "../helpers/const"; -import { isHelperDomain } from "../helpers/const"; -import { HELPERS_CRUD } from "../../../data/helpers_crud"; + computeDeviceName, + getDuplicatedDeviceNames, +} from "../../../common/entity/compute_device_name"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { computeEntityEntryName } from "../../../common/entity/compute_entity_name"; import { computeStateName } from "../../../common/entity/compute_state_name"; +import { + deleteEntity, + isDeletableEntity, +} from "../../../common/entity/delete_entity"; import { PROTOCOL_INTEGRATIONS, protocolIntegrationPicked, @@ -53,7 +56,6 @@ import "../../../components/data-table/ha-data-table-labels"; import "../../../components/ha-alert"; import "../../../components/ha-button-menu"; import "../../../components/ha-check-list-item"; -import "../../../components/ha-md-divider"; import "../../../components/ha-filter-devices"; import "../../../components/ha-filter-domains"; import "../../../components/ha-filter-floor-areas"; @@ -62,6 +64,7 @@ import "../../../components/ha-filter-labels"; import "../../../components/ha-filter-states"; import "../../../components/ha-icon"; import "../../../components/ha-icon-button"; +import "../../../components/ha-md-divider"; import "../../../components/ha-md-menu-item"; import "../../../components/ha-sub-menu"; import "../../../components/ha-svg-icon"; @@ -78,17 +81,15 @@ import type { EntityRegistryEntry, UpdateEntityRegistryEntryResult, } from "../../../data/entity_registry"; -import { - computeEntityRegistryName, - updateEntityRegistryEntry, -} from "../../../data/entity_registry"; -import type { IntegrationManifest } from "../../../data/integration"; -import { - fetchIntegrationManifests, - domainToName, -} from "../../../data/integration"; +import { updateEntityRegistryEntry } from "../../../data/entity_registry"; import type { EntitySources } from "../../../data/entity_sources"; import { fetchEntitySourcesWithCache } from "../../../data/entity_sources"; +import { HELPERS_CRUD } from "../../../data/helpers_crud"; +import type { IntegrationManifest } from "../../../data/integration"; +import { + domainToName, + fetchIntegrationManifests, +} from "../../../data/integration"; import type { LabelRegistryEntry } from "../../../data/label_registry"; import { createLabelRegistryEntry, @@ -106,6 +107,8 @@ import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant, Route } from "../../../types"; import { configSections } from "../ha-panel-config"; +import type { Helper } from "../helpers/const"; +import { isHelperDomain } from "../helpers/const"; import "../integrations/ha-integration-overflow-menu"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; import { showLabelDetailDialog } from "../labels/show-dialog-label-detail"; @@ -124,6 +127,8 @@ export interface EntityRow extends StateEntity { restored: boolean; status: string | undefined; area?: string; + device?: string; + device_full?: string; localized_platform: string; domain: string; label_entries: LabelRegistryEntry[]; @@ -304,11 +309,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { }, name: { main: true, - title: localize("ui.panel.config.entities.picker.headers.name"), + title: localize("ui.panel.config.entities.picker.headers.entity"), sortable: true, filterable: true, direction: "asc", - flex: 2, extraTemplate: (entry) => entry.label_entries.length ? html` @@ -318,10 +322,29 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { ` : nothing, }, + device: { + title: localize("ui.panel.config.entities.picker.headers.device"), + sortable: true, + template: (entry) => entry.device || "—", + }, + device_full: { + title: localize("ui.panel.config.entities.picker.headers.device"), + filterable: true, + groupable: true, + hidden: true, + }, + area: { + title: localize("ui.panel.config.entities.picker.headers.area"), + sortable: true, + filterable: true, + groupable: true, + template: (entry) => entry.area || "—", + }, entity_id: { title: localize("ui.panel.config.entities.picker.headers.entity_id"), sortable: true, filterable: true, + defaultHidden: true, }, localized_platform: { title: localize("ui.panel.config.entities.picker.headers.integration"), @@ -336,12 +359,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { filterable: true, groupable: true, }, - area: { - title: localize("ui.panel.config.entities.picker.headers.area"), - sortable: true, - filterable: true, - groupable: true, - }, disabled_by: { title: localize("ui.panel.config.entities.picker.headers.disabled_by"), hidden: true, @@ -620,12 +637,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { } }); + const duplicatedDevicesNames = getDuplicatedDeviceNames(devices); + for (const entry of filteredEntities) { const entity = this.hass.states[entry.entity_id]; const unavailable = entity?.state === UNAVAILABLE; const restored = entity?.attributes.restored === true; - const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id; + const deviceId = entry.device_id; + const areaId = entry.area_id || devices[deviceId!]?.area_id; const area = areaId ? areas[areaId] : undefined; + const device = deviceId ? devices[deviceId] : undefined; const hidden = !!entry.hidden_by; const disabled = !!entry.disabled_by; const readonly = entry.readonly; @@ -651,17 +672,30 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { (lbl) => labelReg!.find((label) => label.label_id === lbl)! ); + const entityName = computeEntityEntryName( + entry as EntityRegistryEntry, + this.hass + ); + + const deviceName = device ? computeDeviceName(device) : undefined; + const areaName = area ? computeAreaName(area) : undefined; + + const deviceFullName = deviceName + ? duplicatedDevicesNames.has(deviceName) && areaName + ? `${deviceName} (${areaName})` + : deviceName + : undefined; + result.push({ ...entry, entity, - name: computeEntityRegistryName( - this.hass!, - entry as EntityRegistryEntry - ), + name: entityName || deviceName || entry.entity_id, + device: deviceName, + area: areaName, + device_full: deviceFullName, unavailable, restored, localized_platform: domainToName(localize, entry.platform), - area: area ? area.name : "—", domain: domainToName(localize, computeDomain(entry.entity_id)), status: restored ? localize("ui.panel.config.entities.picker.status.not_provided") @@ -792,7 +826,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { } selectable .selected=${this._selected.length} - .initialGroupColumn=${this._activeGrouping} + .initialGroupColumn=${this._activeGrouping ?? "device_full"} .initialCollapsedGroups=${this._activeCollapsed} .initialSorting=${this._activeSorting} .columnOrder=${this._activeColumnOrder} diff --git a/src/translations/en.json b/src/translations/en.json index d476ec3501..309464d647 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4970,7 +4970,7 @@ "disabled": "Disabled", "data_table": { "icon": "Icon", - "name": "Name", + "device": "Device", "manufacturer": "Manufacturer", "model": "Model", "area": "Area", @@ -5017,8 +5017,9 @@ }, "headers": { "state_icon": "State icon", - "name": "Name", + "entity": "Entity", "entity_id": "Entity ID", + "device": "Device", "integration": "Integration", "area": "Area", "disabled_by": "Disabled by",