1
0
mirror of https://github.com/home-assistant/frontend.git synced 2026-04-02 00:27:49 +01:00

Revert entity naming change (#30384)

This commit is contained in:
Paul Bottein
2026-03-27 21:45:54 +01:00
parent 82fc2fccdc
commit 248332ae27
19 changed files with 317 additions and 118 deletions

View File

@@ -4,11 +4,14 @@ import type {
EntityRegistryEntry,
} from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import { computeDeviceName } from "./compute_device_name";
import { computeStateName } from "./compute_state_name";
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
export const computeEntityName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"]
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): string | undefined => {
const entry = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
@@ -18,22 +21,49 @@ export const computeEntityName = (
// Fall back to state name if not in the entity registry (friendly name)
return computeStateName(stateObj);
}
return computeEntityEntryName(entry);
return computeEntityEntryName(entry, devices);
};
export const computeEntityEntryName = (
entry: EntityRegistryDisplayEntry | EntityRegistryEntry
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
devices: HomeAssistant["devices"],
fallbackStateObj?: HassEntity
): string | undefined => {
if (entry.name != null) {
return entry.name;
const name =
entry.name ||
("original_name" in entry && entry.original_name != null
? String(entry.original_name)
: undefined);
const device = entry.device_id ? devices[entry.device_id] : undefined;
if (!device) {
if (name) {
return name;
}
if (fallbackStateObj) {
return computeStateName(fallbackStateObj);
}
return undefined;
}
if ("original_name" in entry && entry.original_name != null) {
return String(entry.original_name);
const deviceName = computeDeviceName(device);
// If the device name is the same as the entity name, consider empty entity name
if (deviceName === name) {
return undefined;
}
return undefined;
// Remove the device name from the entity name if it starts with it
if (deviceName && name) {
return stripPrefixFromEntityName(name, deviceName) || name;
}
return name;
};
export const entityUseDeviceName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"]
): boolean => !computeEntityName(stateObj, entities);
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): boolean => !computeEntityName(stateObj, entities, devices);

View File

@@ -5,6 +5,7 @@ import { computeAreaName } from "./compute_area_name";
import { computeDeviceName } from "./compute_device_name";
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
import { computeFloorName } from "./compute_floor_name";
import { computeStateName } from "./compute_state_name";
import { getEntityContext } from "./context/get_entity_context";
const DEFAULT_SEPARATOR = " ";
@@ -40,7 +41,12 @@ export const computeEntityNameDisplay = (
return name;
}
let items = ensureArray(name ?? DEFAULT_ENTITY_NAME);
// If no name config is provided, fall back to the friendly name
if (!name) {
return computeStateName(stateObj);
}
let items = ensureArray(name);
const separator = options?.separator ?? DEFAULT_SEPARATOR;
@@ -49,7 +55,7 @@ export const computeEntityNameDisplay = (
return items.map((item) => item.text).join(separator);
}
const useDeviceName = entityUseDeviceName(stateObj, entities);
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
// If entity uses device name, and device is not already included, replace it with device name
if (useDeviceName) {
@@ -95,7 +101,7 @@ export const computeEntityNameList = (
const names = name.map((item) => {
switch (item.type) {
case "entity":
return computeEntityName(stateObj, entities);
return computeEntityName(stateObj, entities, devices);
case "device":
return device ? computeDeviceName(device) : undefined;
case "area":

View File

@@ -7,10 +7,7 @@ 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 {
DEFAULT_ENTITY_NAME,
type EntityNameItem,
} from "../../common/entity/compute_entity_name_display";
import type { EntityNameItem } 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";
@@ -333,13 +330,13 @@ export class HaEntityNamePicker extends LitElement {
}
return [{ type: "text", text: value } satisfies EntityNameItem];
}
return value ? ensureArray(value) : [...DEFAULT_ENTITY_NAME];
return value ? ensureArray(value) : [];
});
private _toValue = memoizeOne(
(items: EntityNameItem[]): typeof this.value => {
if (items.length === 0) {
return "";
return undefined;
}
if (items.length === 1) {
const item = items[0];

View File

@@ -535,7 +535,7 @@ export class HaTargetPickerItemRow extends LitElement {
const stateObject: HassEntity | undefined = this.hass.states[item];
const entityName = stateObject
? computeEntityName(stateObject, this.hass.entities)
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
: item;
const { area, device } = stateObject
? getEntityContext(

View File

@@ -536,9 +536,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
: undefined;
const entityName = stateObj
? computeEntityName(stateObj, this.hass.entities)
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
: this._entry
? computeEntityEntryName(this._entry)
? computeEntityEntryName(this._entry, this.hass.devices)
: entityId;
const deviceName = context?.device

View File

@@ -107,7 +107,11 @@ class MoreInfoContent extends LitElement {
if (!stateObj) {
return null;
}
const entityName = computeEntityName(stateObj, hass.entities);
const entityName = computeEntityName(
stateObj,
hass.entities,
hass.devices
);
const { area } = getEntityContext(
stateObj,
hass.entities,

View File

@@ -18,7 +18,7 @@ import {
computeSection,
} from "../../../lovelace/common/generate-lovelace-config";
import { addEntitiesToLovelaceView } from "../../../lovelace/editor/add-entities-to-view";
import type { EntityRegistryEntryWithDisplayName } from "../ha-config-device-page";
import type { EntityRegistryStateEntry } from "../ha-config-device-page";
import { entityRowElement } from "../../../lovelace/entity-rows/entity-row-element-directive";
@customElement("ha-device-entities-card")
@@ -30,7 +30,7 @@ export class HaDeviceEntitiesCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public entities!: EntityRegistryEntryWithDisplayName[];
public entities!: EntityRegistryStateEntry[];
@property({ attribute: "show-hidden", type: Boolean })
public showHidden = false;
@@ -115,10 +115,8 @@ export class HaDeviceEntitiesCard extends LitElement {
this.showHidden = !this.showHidden;
}
private _renderEntity(
entry: EntityRegistryEntryWithDisplayName
): TemplateResult {
let name = entry.display_name || this.deviceName;
private _renderEntity(entry: EntityRegistryStateEntry): TemplateResult {
let name = entry.stateName || this.deviceName;
if (entry.hidden_by) {
name += ` (${this.hass.localize(
"ui.panel.config.devices.entities.hidden"
@@ -130,9 +128,9 @@ export class HaDeviceEntitiesCard extends LitElement {
}
private _renderUnavailableEntity(
entry: EntityRegistryEntryWithDisplayName
entry: EntityRegistryStateEntry
): TemplateResult {
const name = entry.display_name || this.deviceName;
const name = entry.stateName || this.deviceName;
const icon = until(entryIcon(this.hass, entry));

View File

@@ -34,7 +34,6 @@ import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-next";
@@ -66,6 +65,7 @@ import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import {
findBatteryChargingEntity,
findBatteryEntity,
updateEntityRegistryEntry,
} from "../../../data/entity/entity_registry";
import type { IntegrationManifest } from "../../../data/integration";
import { domainToName } from "../../../data/integration";
@@ -94,9 +94,10 @@ import {
loadDeviceRegistryDetailDialog,
showDeviceRegistryDetailDialog,
} from "./device-registry-detail/show-dialog-device-registry-detail";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
export interface EntityRegistryEntryWithDisplayName extends EntityRegistryEntry {
display_name?: string | null;
export interface EntityRegistryStateEntry extends EntityRegistryEntry {
stateName?: string | null;
}
export interface DeviceAction {
@@ -176,17 +177,17 @@ export class HaConfigDevicePage extends LitElement {
(
deviceId: string,
entities: EntityRegistryEntry[]
): EntityRegistryEntry[] =>
): EntityRegistryStateEntry[] =>
entities
.filter((entity) => entity.device_id === deviceId)
.map((entity) => ({
...entity,
display_name: computeEntityEntryName(entity),
stateName: this._computeEntityName(entity),
}))
.sort((ent1, ent2) =>
stringCompare(
ent1.display_name || "",
ent2.display_name || "",
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`,
this.hass.locale.language
)
)
@@ -216,7 +217,7 @@ export class HaConfigDevicePage extends LitElement {
private _deviceIdInList = memoizeOne((deviceId: string) => [deviceId]);
private _entityIds = memoizeOne(
(entries: EntityRegistryEntryWithDisplayName[]): string[] =>
(entries: EntityRegistryStateEntry[]): string[] =>
entries.map((entry) => entry.entity_id)
);
@@ -249,7 +250,7 @@ export class HaConfigDevicePage extends LitElement {
| "assist"
| "notify"
| NonNullable<EntityRegistryEntry["entity_category"]>,
EntityRegistryEntryWithDisplayName[]
EntityRegistryStateEntry[]
>;
for (const key of [
"assist",
@@ -1270,6 +1271,14 @@ export class HaConfigDevicePage extends LitElement {
}
}
private _computeEntityName(entity: EntityRegistryEntry) {
const device = this.hass.devices[this.deviceId];
return (
computeEntityEntryName(entity, this.hass.devices) ||
computeDeviceNameDisplay(device, this.hass)
);
}
private _onImageLoad(ev) {
ev.target.style.display = "inline-block";
}
@@ -1362,6 +1371,8 @@ export class HaConfigDevicePage extends LitElement {
showDeviceRegistryDetailDialog(this, {
device,
updateEntry: async (updates) => {
const oldDeviceName = device.name_by_user || device.name;
const newDeviceName = updates.name_by_user;
const disabled =
updates.disabled_by === "user" && device.disabled_by !== "user";
@@ -1433,7 +1444,43 @@ export class HaConfigDevicePage extends LitElement {
),
text: err.message,
});
return;
}
if (
!oldDeviceName ||
!newDeviceName ||
oldDeviceName === newDeviceName
) {
return;
}
const entities = this._entities(this.deviceId, this._entityReg);
const updateProms = entities.map((entity) => {
const name = entity.name || entity.stateName;
let newName: string | null | undefined;
if (entity.has_entity_name && !entity.name) {
return undefined;
}
if (
entity.has_entity_name &&
(entity.name === oldDeviceName || entity.name === newDeviceName)
) {
// clear name if it matches the device name and it uses the device name (entity naming)
newName = null;
} else if (name && name.includes(oldDeviceName)) {
newName = name.replace(oldDeviceName, newDeviceName);
} else {
return undefined;
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName,
});
});
await Promise.all(updateProms);
},
});
};

View File

@@ -140,9 +140,9 @@ export class DialogVacuumSegmentMapping
: undefined;
const entityName = stateObj
? computeEntityName(stateObj, this.hass.entities)
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
: this._entry
? computeEntityEntryName(this._entry)
? computeEntityEntryName(this._entry, this.hass.devices)
: this._params.entityId;
const deviceName = context?.device

View File

@@ -164,7 +164,7 @@ export class EntitySettingsHelperTab extends LitElement {
}
private async _confirmDeleteItem(): Promise<void> {
const name = computeEntityEntryName(this.entry);
const name = computeEntityEntryName(this.entry, this.hass.devices);
const confirmationText = await getDeleteConfirmationText(
this.hass,
this.entry,

View File

@@ -153,7 +153,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
@property({ attribute: false }) public helperConfigEntry?: ConfigEntry;
@state() private _name!: string | null;
@state() private _name!: string;
@state() private _icon!: string;
@@ -218,7 +218,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
return;
}
this._name = this.entry.name;
this._name = this.entry.name || "";
this._icon = this.entry.icon || "";
this._deviceClass =
this.entry.device_class || this.entry.original_device_class;
@@ -388,27 +388,14 @@ export class EntityRegistrySettingsEditor extends LitElement {
${this.hideName
? nothing
: html`<ha-textfield
class="name"
.value=${this._name ?? this.entry.original_name ?? ""}
.value=${this._name}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.name"
)}
.disabled=${this.disabled}
.placeholder=${this.entry.original_name}
@input=${this._nameChanged}
.iconTrailing=${this._name !== null}
>
${this._name !== null
? html`<div class="layout horizontal" slot="trailingIcon">
<ha-icon-button
@click=${this._restoreName}
.path=${mdiRestore}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.restore_name"
)}
></ha-icon-button>
</div>`
: nothing}
</ha-textfield>`}
></ha-textfield>`}
${this.hideIcon
? nothing
: html`
@@ -1065,7 +1052,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
const params: Partial<EntityRegistryEntryUpdateParams> = {
name: this._name?.trim() ?? null,
name: this._name.trim() || null,
icon: this._icon.trim() || null,
area_id: this._areaId || null,
labels: this._labels || [],
@@ -1331,11 +1318,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._name = ev.target.value;
}
private _restoreName(): void {
fireEvent(this, "change");
this._name = null;
}
private _iconChanged(ev: CustomEvent): void {
fireEvent(this, "change");
this._icon = ev.detail.value;
@@ -1610,13 +1592,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
}
ha-textfield.entityId {
--text-field-prefix-padding-right: 0;
}
ha-textfield.entityId,
ha-textfield.name {
--textfield-icon-trailing-padding: 0;
}
ha-textfield.entityId ha-icon-button,
ha-textfield.name ha-icon-button {
ha-textfield.entityId ha-icon-button {
position: relative;
right: calc(var(--ha-space-2) * -1);
--ha-icon-button-size: 36px;

View File

@@ -213,7 +213,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
private async _confirmDeleteEntry(): Promise<void> {
let name = computeEntityEntryName(this.entry);
let name = computeEntityEntryName(this.entry, this.hass.devices);
if (!name) {
const { device } = getEntityEntryContext(
this.entry,

View File

@@ -32,10 +32,7 @@ import {
getDuplicatedDeviceNames,
} from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain";
import {
computeEntityEntryName,
computeEntityName,
} from "../../../common/entity/compute_entity_name";
import { computeEntityEntryName } from "../../../common/entity/compute_entity_name";
import { computeStateName } from "../../../common/entity/compute_state_name";
import {
deleteEntity,
@@ -693,9 +690,11 @@ export class HaConfigEntities extends LitElement {
(lbl) => labelReg!.find((label) => label.label_id === lbl)!
);
const entityName = entity
? computeEntityName(entity, this.hass.entities)
: computeEntityEntryName(entry as EntityRegistryEntry);
const entityName = computeEntityEntryName(
entry as EntityRegistryEntry,
this.hass.devices,
entity
);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;

View File

@@ -4,7 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeEntityEntryName } from "../../../../../common/entity/compute_entity_name";
import { computeStateName } from "../../../../../common/entity/compute_state_name";
import { stringCompare } from "../../../../../common/string/compare";
import "../../../../../components/entity/state-badge";
import "../../../../../components/ha-area-picker";
@@ -12,13 +12,17 @@ import "../../../../../components/ha-card";
import "../../../../../components/ha-textfield";
import { updateDeviceRegistryEntry } from "../../../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
import { subscribeEntityRegistry } from "../../../../../data/entity/entity_registry";
import {
getAutomaticEntityIds,
subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../../../data/entity/entity_registry";
import type { ZHADevice } from "../../../../../data/zha";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { EntityRegistryEntryWithDisplayName } from "../../../devices/ha-config-device-page";
import type { EntityRegistryStateEntry } from "../../../devices/ha-config-device-page";
@customElement("zha-device-card")
class ZHADeviceCard extends SubscribeMixin(LitElement) {
@@ -34,17 +38,17 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
(
deviceId: string,
entities: EntityRegistryEntry[]
): EntityRegistryEntryWithDisplayName[] =>
): EntityRegistryStateEntry[] =>
entities
.filter((entity) => entity.device_id === deviceId)
.map((entity) => ({
...entity,
display_name: computeEntityEntryName(entity),
stateName: this._computeEntityName(entity),
}))
.sort((ent1, ent2) =>
stringCompare(
ent1.display_name || "",
ent2.display_name || "",
ent1.stateName || `zzz${ent1.entity_id}`,
ent2.stateName || `zzz${ent2.entity_id}`,
this.hass.locale.language
)
)
@@ -85,7 +89,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
? html`
<state-badge
@click=${this._openMoreInfo}
.title=${entity.display_name || ""}
.title=${entity.stateName!}
.hass=${this.hass}
.stateObj=${this.hass!.states[entity.entity_id]}
slot="item-icon"
@@ -116,11 +120,52 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
if (!this.hass || !this.device) {
return;
}
const device = this.device;
const oldDeviceName = device.user_given_name || device.name;
const newDeviceName = event.target.value;
this.device.user_given_name = newDeviceName;
await updateDeviceRegistryEntry(this.hass, this.device.device_reg_id, {
await updateDeviceRegistryEntry(this.hass, device.device_reg_id, {
name_by_user: newDeviceName,
});
if (!oldDeviceName || !newDeviceName || oldDeviceName === newDeviceName) {
return;
}
const entities = this._deviceEntities(device.device_reg_id, this._entities);
const entityIdsMapping = await getAutomaticEntityIds(
this.hass,
entities.map((entity) => entity.entity_id)
);
const updateProms = entities.map((entity) => {
const name = entity.name;
const newEntityId = entityIdsMapping[entity.entity_id];
let newName: string | null | undefined;
if (entity.has_entity_name && !entity.name) {
newName = undefined;
} else if (
entity.has_entity_name &&
(entity.name === oldDeviceName || entity.name === newDeviceName)
) {
// clear name if it matches the device name and it uses the device name (entity naming)
newName = null;
} else if (name && name.includes(oldDeviceName)) {
newName = name.replace(oldDeviceName, newDeviceName);
}
if (newName !== undefined && !newEntityId) {
return undefined;
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
name: newName,
new_entity_id: newEntityId || undefined,
});
});
await Promise.all(updateProms);
}
private _openMoreInfo(ev: MouseEvent): void {
@@ -129,6 +174,13 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
});
}
private _computeEntityName(entity: EntityRegistryEntry): string | null {
if (this.hass.states[entity.entity_id]) {
return computeStateName(this.hass.states[entity.entity_id]);
}
return entity.name;
}
private async _areaPicked(ev: CustomEvent) {
const picker = ev.currentTarget as any;

View File

@@ -890,6 +890,8 @@ class DialogZWaveJSAddNode extends LitElement {
this._step = "rename_device";
const nameChanged = this._device.name !== this._deviceOptions?.name;
if (nameChanged || this._deviceOptions?.area) {
const oldDeviceName = this._device.name;
const newDeviceName = this._deviceOptions!.name;
try {
await updateDeviceRegistryEntry(this.hass, this._device.id, {
name_by_user: this._deviceOptions!.name,
@@ -897,6 +899,8 @@ class DialogZWaveJSAddNode extends LitElement {
});
if (nameChanged) {
// rename entities
const entities = this._entities.filter(
(entity) => entity.device_id === this._device!.id
);
@@ -908,14 +912,33 @@ class DialogZWaveJSAddNode extends LitElement {
await Promise.all(
entities.map((entity) => {
const name = entity.name;
let newName: string | null | undefined;
const newEntityId = entityIdsMapping[entity.entity_id];
if (!newEntityId || newEntityId === entity.entity_id) {
if (entity.has_entity_name && !entity.name) {
newName = undefined;
} else if (
entity.has_entity_name &&
(entity.name === oldDeviceName ||
entity.name === newDeviceName)
) {
// clear name if it matches the device name and it uses the device name (entity naming)
newName = null;
} else if (name && name.includes(oldDeviceName)) {
newName = name.replace(oldDeviceName, newDeviceName);
}
if (
(newName === undefined && !newEntityId) ||
newEntityId === entity.entity_id
) {
return undefined;
}
return updateEntityRegistryEntry(this.hass!, entity.entity_id, {
new_entity_id: newEntityId,
name: newName || name,
new_entity_id: newEntityId || undefined,
});
})
);

View File

@@ -42,7 +42,8 @@ export class HuiEntityEditor extends LitElement {
const stateObj = this.hass.states[item.entity];
const useDeviceName =
stateObj && entityUseDeviceName(stateObj, this.hass.entities);
stateObj &&
entityUseDeviceName(stateObj, this.hass.entities, this.hass.devices);
const isRTL = computeRTL(this.hass);

View File

@@ -1764,7 +1764,6 @@
"faq": "documentation",
"editor": {
"name": "Name",
"restore_name": "Restore default name",
"icon": "Icon",
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
"default_code": "Default code",

View File

@@ -1,9 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import * as computeDeviceNameModule from "../../../src/common/entity/compute_device_name";
import {
computeEntityEntryName,
computeEntityName,
} from "../../../src/common/entity/compute_entity_name";
import * as computeStateNameModule from "../../../src/common/entity/compute_state_name";
import * as stripPrefixModule from "../../../src/common/entity/strip_prefix_from_entity_name";
import type { HomeAssistant } from "../../../src/types";
import {
mockEntity,
@@ -23,8 +25,11 @@ describe("computeEntityName", () => {
});
const hass = {
entities: {},
devices: {},
} as unknown as HomeAssistant;
expect(computeEntityName(stateObj, hass.entities)).toBe("Kitchen Light");
expect(computeEntityName(stateObj, hass.entities, hass.devices)).toBe(
"Kitchen Light"
);
vi.restoreAllMocks();
});
@@ -42,47 +47,77 @@ describe("computeEntityName", () => {
labels: [],
},
},
devices: {},
states: {
"light.kitchen": stateObj,
},
} as unknown as HomeAssistant;
expect(computeEntityName(stateObj, hass.entities)).toBe("Ceiling Light");
expect(computeEntityName(stateObj, hass.entities, hass.devices)).toBe(
"Ceiling Light"
);
});
});
describe("computeEntityEntryName", () => {
it("returns entry.name if present", () => {
it("returns entry.name if no device", () => {
const entry = mockEntity({
entity_id: "light.kitchen",
name: "Ceiling Light",
});
expect(computeEntityEntryName(entry)).toBe("Ceiling Light");
const hass = { devices: {}, states: {} };
expect(computeEntityEntryName(entry, hass.devices)).toBe("Ceiling Light");
});
it("returns entity name as-is when device present", () => {
it("returns device-stripped name if device present", () => {
vi.spyOn(computeDeviceNameModule, "computeDeviceName").mockReturnValue(
"Kitchen"
);
vi.spyOn(stripPrefixModule, "stripPrefixFromEntityName").mockImplementation(
(name, prefix) => name.replace(prefix + " ", "")
);
const entry = mockEntity({
entity_id: "light.kitchen",
name: "Light",
name: "Kitchen Light",
device_id: "dev1",
});
expect(computeEntityEntryName(entry)).toBe("Light");
const hass = {
devices: { dev1: {} },
states: {},
} as unknown as HomeAssistant;
expect(computeEntityEntryName(entry, hass.devices)).toBe("Light");
vi.restoreAllMocks();
});
it("returns undefined if entity has no name (uses device name)", () => {
it("returns undefined if device name equals entity name", () => {
vi.spyOn(computeDeviceNameModule, "computeDeviceName").mockReturnValue(
"Kitchen Light"
);
const entry = mockEntity({
entity_id: "light.kitchen",
name: "Kitchen Light",
device_id: "dev1",
});
expect(computeEntityEntryName(entry)).toBeUndefined();
const hass = {
devices: { dev1: {} },
states: {},
} as unknown as HomeAssistant;
expect(computeEntityEntryName(entry, hass.devices)).toBeUndefined();
vi.restoreAllMocks();
});
it("returns empty string if name is empty (does not fallback to original_name)", () => {
const entry = mockEntityEntry({
entity_id: "light.kitchen",
name: "",
original_name: "Old Name",
});
expect(computeEntityEntryName(entry)).toBe("");
it("falls back to state name if no name and no device", () => {
vi.spyOn(computeStateNameModule, "computeStateName").mockReturnValue(
"Fallback Name"
);
const entry = mockEntity({ entity_id: "light.kitchen" });
const hass = {
devices: {},
} as unknown as HomeAssistant;
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
expect(computeEntityEntryName(entry, hass.devices, stateObj)).toBe(
"Fallback Name"
);
vi.restoreAllMocks();
});
it("returns original_name if present", () => {
@@ -90,15 +125,27 @@ describe("computeEntityEntryName", () => {
entity_id: "light.kitchen",
original_name: "Old Name",
});
expect(computeEntityEntryName(entry)).toBe("Old Name");
const hass = {
devices: {},
states: {},
} as unknown as HomeAssistant;
expect(computeEntityEntryName(entry, hass.devices)).toBe("Old Name");
});
it("returns undefined if no name or original_name", () => {
it("returns undefined if no name, original_name, or device", () => {
const entry = mockEntity({ entity_id: "light.kitchen" });
expect(computeEntityEntryName(entry)).toBeUndefined();
const hass = {
devices: {},
states: {},
} as unknown as HomeAssistant;
expect(computeEntityEntryName(entry, hass.devices)).toBeUndefined();
});
it("handles entities with numeric original_name (real bug from issue #25363)", () => {
vi.spyOn(computeDeviceNameModule, "computeDeviceName").mockReturnValue(
"Texas Instruments CC2652"
);
const entry = {
entity_id: "sensor.texas_instruments_cc2652_2",
name: null, // null name
@@ -106,21 +153,37 @@ describe("computeEntityEntryName", () => {
device_id: "dev1",
has_entity_name: true,
};
const hass = {
devices: { dev1: {} },
states: {},
};
// Should not throw an error and should return the stringified number
expect(() => computeEntityEntryName(entry as any)).not.toThrow();
expect(computeEntityEntryName(entry as any)).toBe("2");
expect(() =>
computeEntityEntryName(entry as any, hass as any)
).not.toThrow();
expect(computeEntityEntryName(entry as any, hass as any)).toBe("2");
vi.restoreAllMocks();
});
it("returns undefined when entity has device but no name or original_name", () => {
vi.spyOn(computeDeviceNameModule, "computeDeviceName").mockReturnValue(
"Kitchen Device"
);
const entry = {
entity_id: "sensor.kitchen_sensor",
// No name property
// No original_name property
device_id: "dev1",
};
const hass = {
devices: { dev1: {} },
states: {},
};
// Should return undefined to maintain function contract
expect(computeEntityEntryName(entry as any)).toBeUndefined();
expect(computeEntityEntryName(entry as any, hass as any)).toBeUndefined();
vi.restoreAllMocks();
});
});

View File

@@ -115,6 +115,7 @@ describe("computeEntityNameDisplay", () => {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Kitchen Device",
device_id: "dev1",
}),
},
@@ -140,12 +141,13 @@ describe("computeEntityNameDisplay", () => {
expect(result).toBe("Kitchen Device");
});
it("does not duplicate device name when entity uses device name and device is included", () => {
it("does not replace entity with device when device is already included", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Kitchen Device",
device_id: "dev1",
}),
},
@@ -168,8 +170,8 @@ describe("computeEntityNameDisplay", () => {
hass.floors
);
// Entity has no name (uses device name), device is already included
// So we only get the device name once
// Since entity name equals device name, entity returns undefined
// So we only get the device name
expect(result).toBe("Kitchen Device");
});