Handle unknown items in target picker (#27795)

* Handle unknown items in target picker

* Update ha-target-picker-item-row.ts

* update colors

* fallback to domain icons
This commit is contained in:
Bram Kragten
2025-11-04 18:02:56 +01:00
parent 3c82d12609
commit d23e45e410
2 changed files with 61 additions and 26 deletions

View File

@@ -6,6 +6,7 @@ import {
mdiLabel, mdiLabel,
mdiTextureBox, mdiTextureBox,
} from "@mdi/js"; } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, type PropertyValues } from "lit"; import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -19,9 +20,12 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name"; import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context"; import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl"; import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area_registry";
import { getConfigEntry } from "../../data/config_entries"; import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context"; import { labelsContext } from "../../data/context";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import { domainToName } from "../../data/integration"; import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label_registry"; import type { LabelRegistryEntry } from "../../data/label_registry";
import { import {
@@ -111,10 +115,10 @@ export class HaTargetPickerItemRow extends LitElement {
} }
protected render() { protected render() {
const { name, context, iconPath, fallbackIconPath, stateObject } = const { name, context, iconPath, fallbackIconPath, stateObject, notFound } =
this._itemData(this.type, this.itemId); this._itemData(this.type, this.itemId);
const showEntities = this.type !== "entity"; const showEntities = this.type !== "entity" && !notFound;
const entries = this.parentEntries || this._entries; const entries = this.parentEntries || this._entries;
@@ -128,7 +132,7 @@ export class HaTargetPickerItemRow extends LitElement {
} }
return html` return html`
<ha-md-list-item type="text"> <ha-md-list-item type="text" class=${notFound ? "error" : ""}>
<div class="icon" slot="start"> <div class="icon" slot="start">
${this.subEntry ${this.subEntry
? html` ? html`
@@ -148,11 +152,15 @@ export class HaTargetPickerItemRow extends LitElement {
/>` />`
: fallbackIconPath : fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>` ? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: stateObject : this.type === "entity"
? html` ? html`
<ha-state-icon <ha-state-icon
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObject} .stateObj=${stateObject ||
({
entity_id: this.itemId,
attributes: {},
} as HassEntity)}
> >
</ha-state-icon> </ha-state-icon>
` `
@@ -160,8 +168,14 @@ export class HaTargetPickerItemRow extends LitElement {
</div> </div>
<div slot="headline">${name}</div> <div slot="headline">${name}</div>
${context && !this.hideContext ${notFound || (context && !this.hideContext)
? html`<span slot="supporting-text">${context}</span>` ? html`<span slot="supporting-text"
>${notFound
? this.hass.localize(
`ui.components.target-picker.${this.type}_not_found`
)
: context}</span
>`
: nothing} : nothing}
${this._domainName && this.subEntry ${this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain" ? html`<span slot="supporting-text" class="domain"
@@ -474,26 +488,28 @@ export class HaTargetPickerItemRow extends LitElement {
private _itemData = memoizeOne((type: TargetType, item: string) => { private _itemData = memoizeOne((type: TargetType, item: string) => {
if (type === "floor") { if (type === "floor") {
const floor = this.hass.floors?.[item]; const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item];
return { return {
name: floor?.name || item, name: floor?.name || item,
iconPath: floor?.icon, iconPath: floor?.icon,
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome, fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
notFound: !floor,
}; };
} }
if (type === "area") { if (type === "area") {
const area = this.hass.areas?.[item]; const area: AreaRegistryEntry | undefined = this.hass.areas?.[item];
return { return {
name: area?.name || item, name: area?.name || item,
context: area.floor_id && this.hass.floors?.[area.floor_id]?.name, context: area?.floor_id && this.hass.floors?.[area.floor_id]?.name,
iconPath: area?.icon, iconPath: area?.icon,
fallbackIconPath: mdiTextureBox, fallbackIconPath: mdiTextureBox,
notFound: !area,
}; };
} }
if (type === "device") { if (type === "device") {
const device = this.hass.devices?.[item]; const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item];
if (device.primary_config_entry) { if (device?.primary_config_entry) {
this._getDeviceDomain(device.primary_config_entry); this._getDeviceDomain(device.primary_config_entry);
} }
@@ -501,24 +517,25 @@ export class HaTargetPickerItemRow extends LitElement {
name: device ? computeDeviceNameDisplay(device, this.hass) : item, name: device ? computeDeviceNameDisplay(device, this.hass) : item,
context: device?.area_id && this.hass.areas?.[device.area_id]?.name, context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
fallbackIconPath: mdiDevices, fallbackIconPath: mdiDevices,
notFound: !device,
}; };
} }
if (type === "entity") { if (type === "entity") {
this._setDomainName(computeDomain(item)); this._setDomainName(computeDomain(item));
const stateObject = this.hass.states[item]; const stateObject: HassEntity | undefined = this.hass.states[item];
const entityName = computeEntityName( const entityName = stateObject
stateObject, ? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
this.hass.entities, : item;
this.hass.devices const { area, device } = stateObject
); ? getEntityContext(
const { area, device } = getEntityContext( stateObject,
stateObject, this.hass.entities,
this.hass.entities, this.hass.devices,
this.hass.devices, this.hass.areas,
this.hass.areas, this.hass.floors
this.hass.floors )
); : { area: undefined, device: undefined };
const deviceName = device ? computeDeviceName(device) : undefined; const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined; const areaName = area ? computeAreaName(area) : undefined;
const context = [areaName, entityName ? deviceName : undefined] const context = [areaName, entityName ? deviceName : undefined]
@@ -528,15 +545,19 @@ export class HaTargetPickerItemRow extends LitElement {
name: entityName || deviceName || item, name: entityName || deviceName || item,
context, context,
stateObject, stateObject,
notFound: !stateObject,
}; };
} }
// type label // type label
const label = this._labelRegistry.find((lab) => lab.label_id === item); const label: LabelRegistryEntry | undefined = this._labelRegistry.find(
(lab) => lab.label_id === item
);
return { return {
name: label?.name || item, name: label?.name || item,
iconPath: label?.icon, iconPath: label?.icon,
fallbackIconPath: mdiLabel, fallbackIconPath: mdiLabel,
notFound: !label,
}; };
}); });
@@ -597,11 +618,20 @@ export class HaTargetPickerItemRow extends LitElement {
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
} }
.error {
background: var(--ha-color-fill-warning-quiet-resting);
}
.error [slot="supporting-text"] {
color: var(--ha-color-on-warning-normal);
}
state-badge { state-badge {
color: var(--ha-color-on-neutral-quiet); color: var(--ha-color-on-neutral-quiet);
} }
.icon { .icon {
width: 24px;
display: flex; display: flex;
} }

View File

@@ -695,6 +695,11 @@
"remove_device_id": "Remove device", "remove_device_id": "Remove device",
"remove_entity_id": "Remove entity", "remove_entity_id": "Remove entity",
"remove_label_id": "Remove label", "remove_label_id": "Remove label",
"floor_not_found": "Floor not found",
"area_not_found": "Area not found",
"device_not_found": "Device not found",
"entity_not_found": "Entity not found",
"label_not_found": "Label not found",
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}", "devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}", "entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"target_details": "Target details", "target_details": "Target details",