Compare commits

...
Author SHA1 Message Date
Paul Bottein 29b2a01299 Add parent device to entity context 2026-08-24 15:59:53 +02:00
27 changed files with 426 additions and 144 deletions
@@ -12,13 +12,24 @@ import { getEntityContext } from "./context/get_entity_context";
const DEFAULT_SEPARATOR = " ";
export const DEFAULT_ENTITY_NAME = [
{ type: "parent_device" },
{ type: "device" },
{ type: "entity" },
] satisfies EntityNameItem[];
export const ENTITY_NAME_TYPES = [
"entity",
"device",
"parent_device",
"area",
"floor",
] as const;
export type EntityNameType = (typeof ENTITY_NAME_TYPES)[number];
export type EntityNameItem =
| {
type: "entity" | "device" | "area" | "floor";
type: EntityNameType;
}
| {
type: "text";
@@ -91,7 +102,7 @@ export const computeEntityNameList = (
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"]
): (string | undefined)[] => {
const { device, area, floor } = getEntityContext(
const { device, parentDevice, area, floor } = getEntityContext(
stateObj,
entities,
devices,
@@ -105,6 +116,8 @@ export const computeEntityNameList = (
return computeEntityName(stateObj, entities, devices);
case "device":
return device ? computeDeviceName(device) : undefined;
case "parent_device":
return parentDevice ? computeDeviceName(parentDevice) : undefined;
case "area":
return area ? computeAreaName(area) : undefined;
case "floor":
@@ -136,14 +149,20 @@ export const computeEntityPickerDisplay = (
>,
stateObj: HassEntity
): EntityPickerDisplay => {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const isRTL = computeRTL(
hass.language,
@@ -152,7 +171,7 @@ export const computeEntityPickerDisplay = (
const primary = entityName || deviceName || stateObj.entity_id;
const secondary =
[areaName, entityName ? deviceName : undefined]
[areaName, parentDeviceName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ") || undefined;
@@ -13,6 +13,7 @@ import { getDeviceAreaId } from "./get_device_context";
interface EntityContext {
entity: EntityRegistryDisplayEntry | null;
device: DeviceRegistryEntry | null;
parentDevice: DeviceRegistryEntry | null;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}
@@ -31,6 +32,7 @@ export const getEntityContext = (
return {
entity: null,
device: null,
parentDevice: null,
area: null,
floor: null,
};
@@ -65,6 +67,9 @@ export const getEntityEntryContext = (
const entity = entities[entry.entity_id];
const deviceId = entry?.device_id;
const device = deviceId ? devices[deviceId] : undefined;
const parentDevice = device?.parent_device_id
? devices[device.parent_device_id]
: undefined;
const areaId =
entry?.area_id || (device ? getDeviceAreaId(device, devices) : undefined);
const area = areaId ? areas[areaId] : undefined;
@@ -74,6 +79,7 @@ export const getEntityEntryContext = (
return {
entity: entity,
device: device || null,
parentDevice: parentDevice || null,
area: area || null,
floor: floor || null,
};
-2
View File
@@ -31,8 +31,6 @@ export type FormatEntityAttributeNameFunc = (
attribute: string
) => string;
export type EntityNameType = "entity" | "device" | "area" | "floor";
export type FormatEntityNameFunc = (
stateObj: HassEntity,
name: EntityNameItem | EntityNameItem[],
@@ -7,9 +7,12 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
import {
ENTITY_NAME_TYPES,
type EntityNameItem,
type EntityNameType,
} from "../../common/entity/compute_entity_name_display";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import type { EntityNameType } from "../../common/translations/entity-state";
import type { LocalizeKeys } from "../../common/translations/localize";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../chips/ha-assist-chip";
@@ -35,9 +38,7 @@ const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
</ha-combo-box-item>
`;
const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
const KNOWN_TYPES = new Set<string>(ENTITY_NAME_TYPES);
const formatOptionValue = (item: EntityNameItem) => {
if (item.type === "text" && item.text) {
@@ -387,6 +388,7 @@ export class HaEntityNamePicker extends LitElement {
);
if (context.device) options.add("device");
if (context.parentDevice) options.add("parent_device");
if (context.area) options.add("area");
if (context.floor) options.add("floor");
return options;
@@ -399,9 +401,7 @@ export class HaEntityNamePicker extends LitElement {
const types = this._validTypes(entityId);
const items = (
["entity", "device", "area", "floor"] as const
).map<PickerComboBoxItem>((name) => {
const items = ENTITY_NAME_TYPES.map<PickerComboBoxItem>((name) => {
const stateObj = this.hass.states[entityId];
const isValid = types.has(name);
const primary = this.hass.localize(
@@ -464,7 +464,7 @@ export class HaEntityNamePicker extends LitElement {
const excludedValues = new Set(
this._items
.filter((item) => UNIQUE_TYPES.has(item.type))
.filter((item) => KNOWN_TYPES.has(item.type))
.map((item) => formatOptionValue(item))
);
@@ -178,6 +178,17 @@ export class HaStateContentPicker extends LitElement {
),
});
}
if (context.parentDevice) {
contextItems.push({
id: "parent_device_name",
primary: this.hass.localize(
"ui.components.state-content-picker.parent_device_name"
),
sorting_label: this.hass.localize(
"ui.components.state-content-picker.parent_device_name"
),
});
}
if (context.area) {
contextItems.push({
id: "area_name",
+41 -18
View File
@@ -52,6 +52,7 @@ const SEARCH_KEYS = [
{ name: "search_labels.entityName", weight: 10 },
{ name: "search_labels.friendlyName", weight: 9 },
{ name: "search_labels.deviceName", weight: 8 },
{ name: "search_labels.parentDeviceName", weight: 6 },
{ name: "search_labels.areaName", weight: 6 },
{ name: "search_labels.domainName", weight: 4 },
{ name: "statisticId", weight: 3 },
@@ -292,17 +293,27 @@ export class HaStatisticPicker extends LitElement {
const friendlyName = computeStateName(stateObj); // Keep this for search
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const primary = entityName || deviceName || id;
const secondary = [areaName, entityName ? deviceName : undefined]
const secondary = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
@@ -318,6 +329,7 @@ export class HaStatisticPicker extends LitElement {
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
parentDeviceName: parentDeviceName || null,
areaName: areaName || null,
friendlyName,
},
@@ -395,14 +407,20 @@ export class HaStatisticPicker extends LitElement {
const stateObj = this.hass.states[statisticId];
if (stateObj) {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const isRTL = computeRTL(
this.hass.language,
@@ -410,7 +428,11 @@ export class HaStatisticPicker extends LitElement {
);
const primary = entityName || deviceName || statisticId;
const secondary = [areaName, entityName ? deviceName : undefined]
const secondary = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const friendlyName = computeStateName(stateObj); // Keep this for search
@@ -427,6 +449,7 @@ export class HaStatisticPicker extends LitElement {
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
parentDeviceName: parentDeviceName || null,
areaName: areaName || null,
friendlyName,
statisticId,
+19 -9
View File
@@ -197,17 +197,27 @@ export class HaAreaControlsPicker extends LitElement {
return;
}
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this.hass!.entities,
this.hass!.devices,
this.hass!.areas,
this.hass!.floors
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
const secondary = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
+12
View File
@@ -761,6 +761,9 @@ export class HaCodeEditor extends ReactiveElement {
const deviceName = context.device
? computeDeviceName(context.device)
: undefined;
const parentDeviceName = context.parentDevice
? computeDeviceName(context.parentDevice)
: undefined;
const areaName = context.area ? computeAreaName(context.area) : undefined;
const floorName = context.floor
? computeFloorName(context.floor)
@@ -788,6 +791,15 @@ export class HaCodeEditor extends ReactiveElement {
});
}
if (parentDeviceName) {
completionItems.push({
label: this._i18n!.localize(
"ui.components.device-picker.parent_device"
),
value: parentDeviceName,
});
}
if (deviceName) {
completionItems.push({
label: this._i18n!.localize("ui.components.device-picker.device"),
@@ -129,21 +129,31 @@ export class HaMediaPlayerPicker extends LitElement {
.filter(this._filterPlayerEntities)
.map<MediaPlayerComboBoxItem>((stateObj) => {
const friendlyName = computeStateName(stateObj);
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this._entities,
this._devices,
this._areas,
this._floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this._entities,
this._devices,
this._areas,
this._floors
);
const entityId = stateObj.entity_id;
const domainName = domainToName(
this._i18n.localize,
computeDomain(entityId)
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
const secondary = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
@@ -157,6 +167,7 @@ export class HaMediaPlayerPicker extends LitElement {
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
parentDeviceName: parentDeviceName || null,
areaName: areaName || null,
domainName: domainName || null,
friendlyName: friendlyName || null,
@@ -62,17 +62,27 @@ class HaMediaPlayerToggle extends LitElement {
isRTL: boolean,
stateObj: HassEntity
) => {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
entities,
devices,
areas,
floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
entities,
devices,
areas,
floors
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
const secondary = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
@@ -683,7 +683,7 @@ export class HaTargetPickerItemRow extends LitElement {
const entityName = stateObject
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
: item;
const { area, device } = stateObject
const { area, device, parentDevice } = stateObject
? getEntityContext(
stateObject,
this.hass.entities,
@@ -691,10 +691,17 @@ export class HaTargetPickerItemRow extends LitElement {
this.hass.areas,
this.hass.floors
)
: { area: undefined, device: undefined };
: { area: undefined, device: undefined, parentDevice: undefined };
const deviceName = device ? computeDeviceName(device) : undefined;
const parentDeviceName = parentDevice
? computeDeviceName(parentDevice)
: undefined;
const areaName = area ? computeAreaName(area) : undefined;
const context = [areaName, entityName ? deviceName : undefined]
const context = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(
computeRTL(
+24 -9
View File
@@ -31,6 +31,10 @@ export const entityComboBoxKeys: FuseWeightedKey[] = [
name: "search_labels.deviceName",
weight: 7,
},
{
name: "search_labels.parentDeviceName",
weight: 6,
},
{
name: "search_labels.areaName",
weight: 6,
@@ -129,14 +133,20 @@ export const getEntities = (
const stateObj = hass.states[entityId];
const friendlyName = computeStateName(stateObj); // Keep this for search
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const domain = computeDomain(entityId);
let domainName = domainNames.get(domain);
@@ -146,7 +156,11 @@ export const getEntities = (
}
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
const secondary = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
@@ -159,6 +173,7 @@ export const getEntities = (
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
parentDeviceName: parentDeviceName || null,
areaName: areaName || null,
domainName: domainName || null,
friendlyName: friendlyName || null,
+2 -4
View File
@@ -1,5 +1,6 @@
import { ensureArray } from "../../common/array/ensure-array";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import type { HomeAssistant } from "../../types";
import type { Selector } from "../selector";
@@ -82,10 +83,7 @@ export const formatSelectorValue = (
if (!stateObj) {
return entityId;
}
const name = hass.formatEntityName(stateObj, [
{ type: "device" },
{ type: "entity" },
]);
const name = hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
return name || entityId;
})
.join(", ");
+16 -1
View File
@@ -74,7 +74,7 @@ class HaMoreInfoDetails extends LitElement {
attributes,
yamlData: stateYamlData,
} = this._getDetailData(this._stateObj);
const { floor, area, device } = getEntityContext(
const { floor, area, device, parentDevice } = getEntityContext(
this._stateObj,
this.hass.entities,
this.hass.devices,
@@ -83,6 +83,13 @@ class HaMoreInfoDetails extends LitElement {
);
const floorName = floor ? computeFloorName(floor) : undefined;
const areaName = area ? (computeAreaName(area) ?? area.area_id) : undefined;
const parentDeviceName = parentDevice
? computeDeviceNameDisplay(
parentDevice,
this.hass.localize,
this.hass.states
)
: undefined;
const deviceName = device
? computeDeviceNameDisplay(device, this.hass.localize, this.hass.states)
: undefined;
@@ -112,6 +119,13 @@ class HaMoreInfoDetails extends LitElement {
href: `/config/areas/area/${area.area_id}`,
});
}
if (parentDevice && parentDeviceName) {
contextEntries.push({
translationKey: "ui.dialogs.more_info_control.parent_device",
value: parentDeviceName,
href: `/config/devices/device/${parentDevice.id}`,
});
}
if (device && deviceName) {
contextEntries.push({
translationKey: "ui.components.related-items.device",
@@ -145,6 +159,7 @@ class HaMoreInfoDetails extends LitElement {
context: {
...(floorName ? { floor: floorName } : {}),
...(areaName ? { area: areaName } : {}),
...(parentDeviceName ? { parent_device: parentDeviceName } : {}),
...(deviceName ? { device: deviceName } : {}),
...(integrationName ? { integration: integrationName } : {}),
},
+9 -3
View File
@@ -615,11 +615,17 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
const deviceName = context?.device
? computeDeviceName(context.device)
: undefined;
const parentDeviceName = context?.parentDevice
? computeDeviceName(context.parentDevice)
: undefined;
const areaName = context?.area ? computeAreaName(context.area) : undefined;
const breadcrumb = [areaName, deviceName, entityName].filter(
(v): v is string => Boolean(v)
);
const breadcrumb = [
areaName,
parentDeviceName,
deviceName,
entityName,
].filter((v): v is string => Boolean(v));
const defaultTitle = breadcrumb.pop() || entityId;
const addToTitle = this.hass.localize(
"ui.dialogs.more_info_control.add_to.title",
@@ -977,16 +977,26 @@ class DialogAddAutomationElement
);
} else {
const stateObj = this.hass.states[targetId];
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
subtitle = [areaName, entityName ? deviceName : undefined]
subtitle = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(
computeRTL(
@@ -31,6 +31,7 @@ const SEARCH_KEYS = [
{ name: "search_labels.entityName", weight: 10 },
{ name: "search_labels.friendlyName", weight: 9 },
{ name: "search_labels.deviceName", weight: 8 },
{ name: "search_labels.parentDeviceName", weight: 6 },
{ name: "search_labels.areaName", weight: 6 },
{ name: "search_labels.domainName", weight: 4 },
{ name: "id", weight: 2 },
@@ -64,14 +65,20 @@ export class HaEnergyUpstreamDevicePicker extends LitElement {
const stateObj = this.hass.states[statisticId];
if (stateObj) {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const friendlyName = computeStateName(stateObj); // Keep this for search
@@ -89,6 +96,7 @@ export class HaEnergyUpstreamDevicePicker extends LitElement {
search_labels: {
entityName: entityName || null,
deviceName: deviceName || null,
parentDeviceName: parentDeviceName || null,
areaName: areaName || null,
friendlyName,
},
@@ -160,11 +160,17 @@ export class DialogVacuumSegmentMapping
const deviceName = context?.device
? computeDeviceName(context.device)
: undefined;
const parentDeviceName = context?.parentDevice
? computeDeviceName(context.parentDevice)
: undefined;
const areaName = context?.area ? computeAreaName(context.area) : undefined;
const breadcrumb = [areaName, deviceName, entityName].filter(
(v): v is string => Boolean(v)
);
const breadcrumb = [
areaName,
parentDeviceName,
deviceName,
entityName,
].filter((v): v is string => Boolean(v));
return html`
<ha-dialog
@@ -195,7 +195,12 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
const nameList = computeEntityNameList(
entity,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this._registries.entities,
this._registries.devices,
this._registries.areas,
@@ -218,13 +223,18 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
continue;
}
const [, deviceName, areaName] = nameList;
const [, deviceName, parentDeviceName, areaName] = nameList;
if (deviceName?.toLowerCase().includes(lowerFilter)) {
result.push({ entity, nameList });
continue;
}
if (parentDeviceName?.toLowerCase().includes(lowerFilter)) {
result.push({ entity, nameList });
continue;
}
if (areaName?.toLowerCase().includes(lowerFilter)) {
result.push({ entity, nameList });
continue;
@@ -237,14 +247,18 @@ class DialogExposeEntity extends DirtyStateProviderMixin<string[]>()(
private _renderItem = (item: FilteredEntity) => {
const { entity: entityState, nameList } = item;
const [entityName, deviceName, areaName] = nameList;
const [entityName, deviceName, parentDeviceName, areaName] = nameList;
const isRTL = computeRTL(
this._i18n.language,
this._i18n.translationMetadata.translations
);
const primary = entityName || deviceName || entityState.entity_id;
const context = [areaName, entityName ? deviceName : undefined]
const context = [
areaName,
parentDeviceName,
entityName ? deviceName : undefined,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const showEntityId = this._config?.userData?.showEntityIdPicker;
@@ -63,8 +63,8 @@ export class HuiEntityEditor extends LitElement {
this.hass.formatEntityName(
stateObj,
useDeviceName
? [{ type: "area" }]
: [{ type: "area" }, { type: "device" }],
? [{ type: "area" }, { type: "parent_device" }]
: [{ type: "area" }, { type: "parent_device" }, { type: "device" }],
{
separator: isRTL ? " ◂ " : " ▸ ",
}
@@ -32,6 +32,7 @@ interface EntityPickerTableRowData extends DataTableRowData {
name: string;
entity_name?: string;
device_name?: string;
parent_device_name?: string;
area_name?: string;
domain_name: string;
last_changed: string;
@@ -60,14 +61,20 @@ export class HuiEntityPickerTable extends LitElement {
(entity) => {
const stateObj = this.hass.states[entity];
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const name = [deviceName, entityName].filter(Boolean).join(" ");
const domain = computeDomain(entity);
@@ -78,6 +85,7 @@ export class HuiEntityPickerTable extends LitElement {
name: name,
entity_name: entityName,
device_name: deviceName,
parent_device_name: parentDeviceName,
area_name: areaName,
domain_name: domainToName(localize, domain),
last_changed: stateObj!.last_changed,
@@ -152,6 +160,7 @@ export class HuiEntityPickerTable extends LitElement {
entity.entity_name || entity.device_name || entity.entity_id;
const secondary = [
entity.area_name,
entity.parent_device_name,
entity.entity_name ? entity.device_name : undefined,
]
.filter(Boolean)
@@ -191,6 +200,12 @@ export class HuiEntityPickerTable extends LitElement {
hidden: true,
};
columns.parent_device_name = {
title: "parent_device_name",
filterable: true,
hidden: true,
};
columns.area_name = {
title: "area_name",
filterable: true,
@@ -172,14 +172,20 @@ export class HuiHeadingBadgesEditor extends LitElement {
`;
}
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const [entityName, deviceName, parentDeviceName, areaName] =
computeEntityNameList(
stateObj,
[
{ type: "entity" },
{ type: "device" },
{ type: "parent_device" },
{ type: "area" },
],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const isRTL = computeRTL(
this.hass.language,
@@ -187,7 +193,11 @@ export class HuiHeadingBadgesEditor extends LitElement {
);
const primary = entityName || deviceName || entityId;
const secondary = [entityName ? deviceName : undefined, areaName]
const secondary = [
entityName ? deviceName : undefined,
parentDeviceName,
areaName,
]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
@@ -1,4 +1,5 @@
import { array, literal, object, string, union } from "superstruct";
import { array, enums, literal, object, string, union } from "superstruct";
import { ENTITY_NAME_TYPES } from "../../../../common/entity/compute_entity_name_display";
const entityNameItemStruct = union([
object({
@@ -6,12 +7,7 @@ const entityNameItemStruct = union([
text: string(),
}),
object({
type: union([
literal("entity"),
literal("device"),
literal("area"),
literal("floor"),
]),
type: enums(ENTITY_NAME_TYPES),
}),
string(),
]);
+13 -7
View File
@@ -5,6 +5,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { join } from "lit/directives/join";
import { ensureArray } from "../common/array/ensure-array";
import type { EntityNameType } from "../common/entity/compute_entity_name_display";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import {
STRINGS_SEPARATOR_DOT,
@@ -56,6 +57,13 @@ export const DEFAULT_STATE_CONTENT_DOMAINS: Record<string, StateContent> = {
valve: ["state", "current_position"],
};
const NAME_CONTENT_TYPES: Partial<Record<string, EntityNameType>> = {
device_name: "device",
parent_device_name: "parent_device",
area_name: "area",
floor_name: "floor",
};
const TIMESTAMP_STATE_PROPS = ["last_updated", "last_changed"];
const TIMESTAMP_CONTENTS = [...TIMESTAMP_STATE_PROPS, "last_triggered"];
@@ -190,13 +198,11 @@ class StateDisplay extends LitElement {
if (content === "entity-id") {
return stateObj.entity_id;
}
if (
content === "device_name" ||
content === "area_name" ||
content === "floor_name"
) {
const type = content.replace("_name", "") as "device" | "area" | "floor";
return this.hass.formatEntityName(stateObj, { type }) || undefined;
const nameType = NAME_CONTENT_TYPES[content];
if (nameType) {
return (
this.hass.formatEntityName(stateObj, { type: nameType }) || undefined
);
}
let relativeDateTime: string | number | undefined;
+5
View File
@@ -764,10 +764,12 @@
"types": {
"floor": "Floor",
"area": "Area",
"parent_device": "[%key:ui::components::device-picker::parent_device%]",
"device": "Device",
"entity": "Entity",
"area_missing": "No area assigned",
"floor_missing": "No floor assigned",
"parent_device_missing": "No parent device",
"device_missing": "No related device"
},
"mode_composed": "Composed",
@@ -916,6 +918,7 @@
"no_devices": "No devices available",
"no_match": "No devices found for {term}",
"device": "Device",
"parent_device": "Parent device",
"unnamed_device": "Unnamed device",
"no_area": "No area",
"placeholder": "Select a device",
@@ -1446,6 +1449,7 @@
"remaining_time": "Remaining time",
"install_status": "Install status",
"device_name": "Device name",
"parent_device_name": "Parent device name",
"area_name": "Area name",
"floor_name": "Floor name"
},
@@ -1673,6 +1677,7 @@
"context": "Context",
"entity": "Entity",
"floor": "Floor",
"parent_device": "[%key:ui::components::device-picker::parent_device%]",
"entity_id": "Entity ID",
"labels": "Labels",
"toggle_yaml_mode": "Toggle YAML mode",
@@ -245,6 +245,43 @@ describe("computeEntityNameDisplay", () => {
expect(result).toBe("Kitchen Smart Light");
});
it("returns parent device name for an entity on a child device", () => {
const stateObj = mockStateObj({ entity_id: "switch.outlet_1" });
const hass = {
entities: {
"switch.outlet_1": mockEntity({
entity_id: "switch.outlet_1",
name: "Switch",
device_id: "child_1",
}),
},
devices: {
child_1: mockDevice({
id: "child_1",
name: "Outlet 1",
parent_device_id: "parent_1",
}),
parent_1: mockDevice({
id: "parent_1",
name: "Power strip",
}),
},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[{ type: "parent_device" }, { type: "device" }, { type: "entity" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("Power strip Outlet 1 Switch");
});
it("returns floor name", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
@@ -37,6 +37,7 @@ describe("getEntityContext", () => {
expect(result).toEqual({
entity,
device: null,
parentDevice: null,
area: null,
floor: null,
});
@@ -88,6 +89,7 @@ describe("getEntityContext", () => {
expect(result).toEqual({
entity,
device,
parentDevice: null,
area,
floor,
});
@@ -144,6 +146,7 @@ describe("getEntityContext", () => {
expect(result).toEqual({
entity,
device: childDevice,
parentDevice,
area,
floor,
});
@@ -184,6 +187,7 @@ describe("getEntityContext", () => {
expect(result).toEqual({
entity,
device: null,
parentDevice: null,
area,
floor,
});
@@ -223,8 +227,38 @@ describe("getEntityContext", () => {
expect(result).toEqual({
entity,
device: null,
parentDevice: null,
area,
floor: null,
});
});
it("should not resolve a parent device for an entity on a main device", () => {
const entity = mockEntity({
entity_id: "switch.strip",
device_id: "parent_1",
});
const parentDevice = mockDevice({
id: "parent_1",
});
const stateObj = mockStateObj({
entity_id: "switch.strip",
});
const result = getEntityContext(
stateObj,
{ "switch.strip": entity },
{ parent_1: parentDevice },
{},
{}
);
expect(result).toEqual({
entity,
device: parentDevice,
parentDevice: null,
area: null,
floor: null,
});
});
});