Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate entity, device and area name in more info dialog header and entity picker #21951

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/common/entity/compute_area_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { AreaRegistryEntry } from "../../data/area_registry";

Check failure on line 1 in src/common/entity/compute_area_name.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

All imports in the declaration are only used as types. Use `import type`

export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
area.name?.trim();
5 changes: 5 additions & 0 deletions src/common/entity/compute_device_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DeviceRegistryEntry } from "../../data/device_registry";

Check failure on line 1 in src/common/entity/compute_device_name.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

All imports in the declaration are only used as types. Use `import type`

export const computeDeviceName = (
device: DeviceRegistryEntry
): string | undefined => (device.name_by_user || device.name)?.trim();
107 changes: 107 additions & 0 deletions src/common/entity/compute_entity_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { HassEntity } from "home-assistant-js-websocket";

Check failure on line 1 in src/common/entity/compute_entity_name.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

All imports in the declaration are only used as types. Use `import type`
import { EntityRegistryDisplayEntry } from "../../data/entity_registry";

Check failure on line 2 in src/common/entity/compute_entity_name.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

All imports in the declaration are only used as types. Use `import type`
import { HomeAssistant } from "../../types";

Check failure on line 3 in src/common/entity/compute_entity_name.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

All imports in the declaration are only used as types. Use `import type`
import { stripPrefixFromEntityName } from "./strip_prefix_from_entity_name";
import { computeStateName } from "./compute_state_name";
import { computeDeviceName } from "./compute_device_name";
import { computeAreaName } from "./compute_area_name";
import { computeFloorName } from "./compute_floor_name";

export const computeEntityFullName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): string | undefined => {
const entry = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;

const entityName = computeEntityName(stateObj, entities, devices);

if (!entry?.has_entity_name) {
return entityName;
}

const deviceName = computeEntityDeviceName(stateObj, entities, devices);

if (!entityName || !deviceName || entityName === deviceName) {
return entityName || deviceName;
}

return `${deviceName} ${entityName}`;
};

export const computeEntityName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): string | undefined => {
const entry = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;

const device = entry?.device_id ? devices[entry.device_id] : undefined;

const name = (entry ? entry.name : computeStateName(stateObj))?.trim();

const deviceName = device ? computeDeviceName(device) : undefined;

if (!name || !deviceName) {
return name || deviceName;
}

if (name === deviceName) {
return name;
}

return stripPrefixFromEntityName(name, deviceName.toLowerCase()) || name;
};

export const computeEntityDeviceName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): string | undefined => {
const entry = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
const device = entry?.device_id ? devices[entry.device_id] : undefined;

return device ? computeDeviceName(device) : undefined;
};

export const computeEntityAreaName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"]
): string | undefined => {
const entry = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
const device = entry?.device_id ? devices[entry?.device_id] : undefined;

const areaId = entry?.area_id || device?.area_id;
const area = areaId ? areas[areaId] : undefined;

return area ? computeAreaName(area) : undefined;
};

export const computeEntityFloorName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"]
): string | undefined => {
const entry = entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
const device = entry?.device_id ? devices[entry?.device_id] : undefined;

const areaId = entry?.area_id || device?.area_id;
const area = areaId ? areas[areaId] : undefined;
const floor = area?.floor_id ? floors[area?.floor_id] : undefined;

return floor ? computeFloorName(floor) : undefined;
};
5 changes: 5 additions & 0 deletions src/common/entity/compute_floor_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { FloorRegistryEntry } from "../../data/floor_registry";

Check failure on line 1 in src/common/entity/compute_floor_name.ts

View workflow job for this annotation

GitHub Actions / Lint and check format

All imports in the declaration are only used as types. Use `import type`

export const computeFloorName = (
floor: FloorRegistryEntry
): string | undefined => floor.name?.trim();
154 changes: 112 additions & 42 deletions src/components/entity/ha-entity-picker.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,37 @@
import "../ha-list-item";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
computeEntityAreaName,
computeEntityDeviceName,
computeEntityFloorName,
computeEntityFullName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import { domainToName } from "../../data/integration";
import type { HelperDomain } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-icon-button";
import "../ha-list-item";
import "../ha-svg-icon";
import "./state-badge";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import { domainToName } from "../../data/integration";
import type { HelperDomain } from "../../panels/config/helpers/const";
import { isHelperDomain } from "../../panels/config/helpers/const";

interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
friendly_name: string;
displayed_name: string;
entity_name?: string;
entity_context?: string;
}

export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
Expand Down Expand Up @@ -105,7 +113,7 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) public hideClearIcon = false;

@property({ attribute: "item-label-path" }) public itemLabelPath =
"friendly_name";
"displayed_name";

@state() private _opened = false;

Expand All @@ -127,22 +135,36 @@ export class HaEntityPicker extends LitElement {

private _rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (
item
) =>
html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
) => html`
<ha-list-item
graphic="avatar"
.twoline=${!!item.entity_id}
multiline-secondary
>
${item.state
? html`<state-badge
slot="graphic"
.stateObj=${item}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary"
>${item.entity_id.startsWith(CREATE_ID)
? html`
<state-badge
slot="graphic"
.stateObj=${item}
.hass=${this.hass}
></state-badge>
`
: nothing}
<span>${item.entity_name ?? item.displayed_name}</span>
${item.entity_context
? html`
<div slot="secondary" style="margin-bottom: 6px">
${item.entity_context}
</div>
`
: nothing}
<div slot="secondary">
${item.entity_id.startsWith(CREATE_ID)
? this.hass.localize("ui.components.entity.entity-picker.new_entity")
: item.entity_id}</span
>
</ha-list-item>`;
: item.entity_id}
</div>
</ha-list-item>
`;

private _getStates = memoizeOne(
(
Expand All @@ -165,8 +187,8 @@ export class HaEntityPicker extends LitElement {
let entityIds = Object.keys(hass.states);

const createItems = createDomains?.length
? createDomains.map((domain) => {
const newFriendlyName = hass.localize(
? createDomains.map<HassEntityWithCachedName>((domain) => {
const displayedName = hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
Expand All @@ -183,11 +205,11 @@ export class HaEntityPicker extends LitElement {
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: newFriendlyName,
displayed_name: displayedName,
attributes: {
icon: "mdi:plus",
},
strings: [domain, newFriendlyName],
strings: [domain, displayedName],
};
})
: [];
Expand All @@ -200,7 +222,7 @@ export class HaEntityPicker extends LitElement {
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
displayed_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_entities"
),
attributes: {
Expand Down Expand Up @@ -240,18 +262,11 @@ export class HaEntityPicker extends LitElement {
}

states = entityIds
.map((key) => {
const friendly_name = computeStateName(hass!.states[key]) || key;
return {
...hass!.states[key],
friendly_name,
strings: [key, friendly_name],
};
})
.map((key) => this._stateObjToRowItem(hass!.states[key], hass))
.sort((entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.friendly_name,
entityB.friendly_name,
entityA.displayed_name,
entityB.displayed_name,
this.hass.locale.language
)
);
Expand Down Expand Up @@ -294,7 +309,7 @@ export class HaEntityPicker extends LitElement {
last_changed: "",
last_updated: "",
context: { id: "", user_id: null, parent_id: null },
friendly_name: this.hass!.localize(
displayed_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
attributes: {
Expand All @@ -317,6 +332,61 @@ export class HaEntityPicker extends LitElement {
}
);

private _stateObjToRowItem(
stateObj: HassEntity,
hass: HomeAssistant
): HassEntityWithCachedName {
const areaName = computeEntityAreaName(
stateObj,
hass.entities,
hass.devices,
hass.areas
);
const floorName = computeEntityFloorName(
stateObj,
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const deviceName = computeEntityDeviceName(
stateObj,
hass.entities,
hass.devices
);

const entityName = computeEntityName(stateObj, hass.entities, hass.devices);

const displayedName = computeEntityFullName(
stateObj,
hass.entities,
hass.devices
);

// Do not include device name if it's the same as entity name
const entityContext = [
entityName !== deviceName ? deviceName : undefined,
areaName,
floorName,
]
.filter(Boolean)
.join(" ⸱ ");

return {
...stateObj,
displayed_name: displayedName ?? "",
strings: [
stateObj.entity_id,
displayedName ?? "",
areaName ?? "",
deviceName ?? "",
floorName ?? "",
].filter(Boolean),
entity_name: entityName,
entity_context: entityContext,
};
}

protected shouldUpdate(changedProps: PropertyValues) {
if (
changedProps.has("value") ||
Expand Down
2 changes: 2 additions & 0 deletions src/data/entity_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface EntityRegistryDisplayEntry {
translation_key?: string;
platform?: string;
display_precision?: number;
has_entity_name?: boolean;
}

export interface EntityRegistryDisplayEntryResponse {
Expand All @@ -41,6 +42,7 @@ export interface EntityRegistryDisplayEntryResponse {
tk?: string;
hb?: boolean;
dp?: number;
hn?: boolean;
}[];
entity_categories: Record<number, EntityCategory>;
}
Expand Down
Loading
Loading