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:
@@ -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);
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user