Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions build-scripts/rspack.cjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
const { existsSync } = require("fs");
const path = require("path");
const rspack = require("@rspack/core");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { RsdoctorRspackPlugin } = require("@rsdoctor/rspack-plugin");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { StatsWriterPlugin } = require("webpack-stats-plugin");
const filterStats = require("@bundle-stats/plugin-webpack-filter");
// eslint-disable-next-line @typescript-eslint/naming-convention
const TerserPlugin = require("terser-webpack-plugin");
// eslint-disable-next-line @typescript-eslint/naming-convention
const { WebpackManifestPlugin } = require("rspack-manifest-plugin");
const log = require("fancy-log");
// eslint-disable-next-line @typescript-eslint/naming-convention
const WebpackBar = require("webpackbar/rspack");
const paths = require("./paths.cjs");
const bundle = require("./bundle.cjs");
Expand Down Expand Up @@ -192,6 +197,7 @@ const createRspackConfig = ({
"lit/directives/if-defined$": "lit/directives/if-defined.js",
"lit/directives/guard$": "lit/directives/guard.js",
"lit/directives/cache$": "lit/directives/cache.js",
"lit/directives/join$": "lit/directives/join.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/directives/keyed$": "lit/directives/keyed.js",
Expand Down
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 type { AreaRegistryEntry } from "../../data/area_registry";

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

export const computeDeviceName = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, in my opinion this function does too much with the entities fallback and translation fallback.

Copy link
Member

@bramkragten bramkragten Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

entities is optional, we could make the translation fallback optional too. We can also wrap the functions (have a computeDeviceNameWithFallback()), but we can not have 2 functions with the same name in the code base, that will go wrong with importing and is highly confusing.

device: DeviceRegistryEntry
): string | undefined => (device.name_by_user || device.name)?.trim();

export const computeDeviceNameDisplay = (
device: DeviceRegistryEntry,
hass: HomeAssistant,
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) =>
computeDeviceName(device) ||
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device", {
type: hass.localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
),
});

export const fallbackDeviceName = (
hass: HomeAssistant,
entities: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) => {
for (const entity of entities || []) {
const entityId = typeof entity === "string" ? entity : entity.entity_id;
const stateObj = hass.states[entityId];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this should use entry.name || entry.original_name first, but maybe for later

if (stateObj) {
return computeStateName(stateObj);
}
}
return undefined;
};
52 changes: 52 additions & 0 deletions src/common/entity/compute_entity_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "../../data/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,
hass: HomeAssistant
): string | undefined => {
const entry = hass.entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;

if (!entry) {
// Fall back to state name if not in the entity registry (friendly name)
return computeStateName(stateObj);
}
return computeEntityEntryName(entry, hass);
};

export const computeEntityEntryName = (
entry: EntityRegistryDisplayEntry | EntityRegistryEntry,
hass: HomeAssistant
): string | undefined => {
const name =
entry.name || ("original_name" in entry ? entry.original_name : undefined);

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

if (!device) {
return 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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it weird that if we request the entity name we get undefined if the name is the same as the device? I feel like the default behavior of computeEntityName should return the name always, and not returning it would be another function or option?

}

// Remove the device name from the entity name if it starts with it
if (deviceName && name) {
return stripPrefixFromEntityName(name, deviceName) || name;
}

return name;
};
4 changes: 4 additions & 0 deletions src/common/entity/compute_floor_name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { FloorRegistryEntry } from "../../data/floor_registry";

export const computeFloorName = (floor: FloorRegistryEntry): string =>
floor.name?.trim();
18 changes: 18 additions & 0 deletions src/common/entity/get_area_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";

interface AreaContext {
floor: FloorRegistryEntry | null;
}
export const getAreaContext = (
area: AreaRegistryEntry,
hass: HomeAssistant
): AreaContext => {
const floorId = area.floor_id;
const floor = floorId ? hass.floors[floorId] : null;

return {
floor: floor,
};
};
24 changes: 24 additions & 0 deletions src/common/entity/get_device_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";

interface DeviceContext {
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}

export const getDeviceContext = (
device: DeviceRegistryEntry,
hass: HomeAssistant
): DeviceContext => {
const areaId = device.area_id;
const area = areaId ? hass.areas[areaId] : null;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : null;

return {
area: area,
floor: floor,
};
};
34 changes: 34 additions & 0 deletions src/common/entity/get_entity_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { HomeAssistant } from "../../types";

interface EntityContext {
device: DeviceRegistryEntry | null;
area: AreaRegistryEntry | null;
floor: FloorRegistryEntry | null;
}

export const getEntityContext = (
stateObj: HassEntity,
hass: HomeAssistant
): EntityContext => {
const entry = hass.entities[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;

const deviceId = entry?.device_id;
const device = deviceId ? hass.devices[deviceId] : null;
const areaId = entry?.area_id || device?.area_id;
const area = areaId ? hass.areas[areaId] : null;
const floorId = area?.floor_id;
const floor = floorId ? hass.floors[floorId] : null;

return {
device: device,
area: area,
floor: floor,
};
};
8 changes: 4 additions & 4 deletions src/common/entity/strip_prefix_from_entity_name.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
const SUFFIXES = [" ", ": "];
const SUFFIXES = [" ", ": ", " - "];

/**
* Strips a device name from an entity name.
* @param entityName the entity name
* @param lowerCasedPrefix the prefix to strip, lower cased
* @param prefix the prefix to strip
* @returns
*/
export const stripPrefixFromEntityName = (
entityName: string,
lowerCasedPrefix: string
prefix: string
) => {
const lowerCasedEntityName = entityName.toLowerCase();

const lowerCasedPrefix = prefix.toLowerCase();
for (const suffix of SUFFIXES) {
const lowerCasedPrefixWithSuffix = `${lowerCasedPrefix}${suffix}`;

Expand Down
8 changes: 3 additions & 5 deletions src/components/device/ha-device-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { LitElement, html } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { stringCompare } from "../../common/string/compare";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
Expand All @@ -13,10 +14,7 @@ import type {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
} from "../../data/device_registry";
import {
computeDeviceName,
getDeviceEntityDisplayLookup,
} from "../../data/device_registry";
import { getDeviceEntityDisplayLookup } from "../../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../../data/entity_registry";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-combo-box";
Expand Down Expand Up @@ -214,7 +212,7 @@ export class HaDevicePicker extends LitElement {
}

const outputDevices = inputDevices.map((device) => {
const name = computeDeviceName(
const name = computeDeviceNameDisplay(
device,
this.hass,
deviceEntityLookup[device.id]
Expand Down
12 changes: 7 additions & 5 deletions src/components/ha-filter-devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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 { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { stringCompare } from "../common/string/compare";
import { computeDeviceName } from "../data/device_registry";
import type { RelatedResult } from "../data/search";
import { findRelated } from "../data/search";
import { haStyleScrollbar } from "../resources/styles";
Expand Down Expand Up @@ -95,7 +95,7 @@ export class HaFilterDevices extends LitElement {
.value=${device.id}
.selected=${this.value?.includes(device.id) ?? false}
>
${computeDeviceName(device, this.hass)}
${computeDeviceNameDisplay(device, this.hass)}
</ha-check-list-item>`;

private _handleItemClick(ev) {
Expand Down Expand Up @@ -142,12 +142,14 @@ export class HaFilterDevices extends LitElement {
.filter(
(device) =>
!filter ||
computeDeviceName(device, this.hass).toLowerCase().includes(filter)
computeDeviceNameDisplay(device, this.hass)
.toLowerCase()
.includes(filter)
)
.sort((a, b) =>
stringCompare(
computeDeviceName(a, this.hass),
computeDeviceName(b, this.hass),
computeDeviceNameDisplay(a, this.hass),
computeDeviceNameDisplay(b, this.hass),
this.hass.locale.language
)
);
Expand Down
6 changes: 4 additions & 2 deletions src/components/ha-target-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import { computeCssColor } from "../common/color/compute-color";
import { hex2rgb } from "../common/color/convert-color";
import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { isValidEntityId } from "../common/entity/valid_entity_id";
import type { AreaRegistryEntry } from "../data/area_registry";
import type { DeviceRegistryEntry } from "../data/device_registry";
import { computeDeviceName } from "../data/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
import type { LabelRegistryEntry } from "../data/label_registry";
import { subscribeLabelRegistry } from "../data/label_registry";
Expand Down Expand Up @@ -150,7 +150,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return this._renderChip(
"device_id",
device_id,
device ? computeDeviceName(device, this.hass) : device_id,
device
? computeDeviceNameDisplay(device, this.hass)
: device_id,
undefined,
undefined,
mdiDevices
Expand Down
14 changes: 0 additions & 14 deletions src/data/device_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,6 @@ export const fallbackDeviceName = (
return undefined;
};

export const computeDeviceName = (
device: DeviceRegistryEntry,
hass: HomeAssistant,
entities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[] | string[]
) =>
device.name_by_user ||
device.name ||
(entities && fallbackDeviceName(hass, entities)) ||
hass.localize("ui.panel.config.devices.unnamed_device", {
type: hass.localize(
`ui.panel.config.devices.type.${device.entry_type || "device"}`
),
});

export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
devices.filter((device) => device.area_id === areaId);

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 @@ -24,6 +24,7 @@ export interface EntityRegistryDisplayEntry {
translation_key?: string;
platform?: string;
display_precision?: number;
has_entity_name?: boolean;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem used now right?

Copy link
Member Author

@piitaya piitaya Mar 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was used in entity picker but I removed it from this PR. I will be useful if we want to compute something similar to friendly_name in the front-end directly.

}

export interface EntityRegistryDisplayEntryResponse {
Expand All @@ -39,6 +40,7 @@ export interface EntityRegistryDisplayEntryResponse {
tk?: string;
hb?: boolean;
dp?: number;
hn?: boolean;
}[];
entity_categories: Record<number, EntityCategory>;
}
Expand Down
4 changes: 2 additions & 2 deletions src/data/script_i18n.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { ensureArray } from "../common/array/ensure-array";
import { formatNumericDuration } from "../common/datetime/format_duration";
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { computeStateName } from "../common/entity/compute_state_name";
import { formatListWithAnds } from "../common/string/format-list";
import { isTemplate } from "../common/string/has-template";
import type { HomeAssistant } from "../types";
import type { Condition } from "./automation";
import { describeCondition } from "./automation_i18n";
import { localizeDeviceAutomationAction } from "./device_automation";
import { computeDeviceName } from "./device_registry";
import type { EntityRegistryEntry } from "./entity_registry";
import {
computeEntityRegistryName,
Expand Down Expand Up @@ -147,7 +147,7 @@ const tryDescribeAction = <T extends ActionType>(
} else if (key === "device_id") {
const device = hass.devices[targetThing];
if (device) {
targets.push(computeDeviceName(device, hass));
targets.push(computeDeviceNameDisplay(device, hass));
} else {
targets.push(
hass.localize(
Expand Down
Loading
Loading