Compare commits

..
Author SHA1 Message Date
Paul Bottein aba8ff5c3d Rename "Use device name" to "Main entity of the device" 2026-10-26 18:29:23 +01:00
6 changed files with 95 additions and 177 deletions

No files matched your search

@@ -5,31 +5,15 @@ import {
mdiPencil,
} from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { HassEntities } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { consume } from "../../common/decorators/consume";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import {
formattersContext,
internationalizationContext,
registriesContext,
statesContext,
} from "../../data/context";
import type { ObjectSelector } from "../../data/selector";
import {
formatSelectorValue,
type FormatSelectorValueOptions,
} from "../../data/selector/format_selector_value";
import { formatSelectorValue } from "../../data/selector/format_selector_value";
import { showFormDialog } from "../../dialogs/form/show-form-dialog";
import type {
HomeAssistantFormatters,
HomeAssistantInternationalization,
HomeAssistantRegistries,
} from "../../types";
import type { HomeAssistant } from "../../types";
import { computeInitialHaFormData } from "../ha-form/compute-initial-ha-form-data";
import type { HaFormSchema } from "../ha-form/types";
import "../ha-input-helper-text";
@@ -42,6 +26,8 @@ import { deepEqual } from "../../common/util/deep-equal";
@customElement("ha-selector-object")
export class HaObjectSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ObjectSelector;
@property() public value?: any;
@@ -61,28 +47,6 @@ export class HaObjectSelector extends LitElement {
values?: Record<string, string>
) => string;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: HomeAssistantInternationalization;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries!: HomeAssistantRegistries;
@state()
@consume({ context: statesContext, subscribe: true })
private _states!: HassEntities;
@state()
@consume({ context: formattersContext, subscribe: true })
@transform<
HomeAssistantFormatters,
HomeAssistantFormatters["formatEntityName"]
>({
transformer: (formatters) => formatters?.formatEntityName,
})
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
@query("ha-yaml-editor", true) private _yamlEditor?: HaYamlEditor;
private _valueChangedFromChild = false;
@@ -119,12 +83,7 @@ export class HaObjectSelector extends LitElement {
private _formatField(item: any, field: string): string {
const fields = this.selector.object!.fields!;
const value = formatSelectorValue(
this._formatOptions(
this._i18n,
this._registries,
this._states,
this._formatEntityName
),
this.hass,
item[field],
fields[field]?.selector
);
@@ -189,14 +148,14 @@ export class HaObjectSelector extends LitElement {
slot="end"
.item=${item}
.index=${index}
.label=${this._i18n.localize("ui.common.edit")}
.label=${this.hass.localize("ui.common.edit")}
.path=${mdiPencil}
@click=${this._editItem}
></ha-icon-button>
<ha-icon-button
slot="end"
.index=${index}
.label=${this._i18n.localize("ui.common.delete")}
.label=${this.hass.localize("ui.common.delete")}
.path=${multiple ? mdiDelete : mdiClose}
@click=${this._deleteItem}
></ha-icon-button>
@@ -221,7 +180,7 @@ export class HaObjectSelector extends LitElement {
</ha-list-base>
</ha-sortable>
<ha-button appearance="filled" @click=${this._addItem}>
${this._i18n.localize("ui.common.add")}
${this.hass.localize("ui.common.add")}
</ha-button>
</div>
`;
@@ -237,7 +196,7 @@ export class HaObjectSelector extends LitElement {
</ha-list-base>`
: html`
<ha-button appearance="filled" @click=${this._addItem}>
${this._i18n.localize("ui.common.add")}
${this.hass.localize("ui.common.add")}
</ha-button>
`
}
@@ -260,23 +219,6 @@ export class HaObjectSelector extends LitElement {
} `;
}
private _formatOptions = memoizeOne(
(
i18n: HomeAssistantInternationalization,
registries: HomeAssistantRegistries,
states: HassEntities,
formatEntityName: HomeAssistantFormatters["formatEntityName"]
): FormatSelectorValueOptions => ({
locale: i18n.locale,
localize: i18n.localize,
states,
devices: registries.devices,
areas: registries.areas,
floors: registries.floors,
formatEntityName,
})
);
private _schema = memoizeOne((selector: ObjectSelector) => {
if (!selector.object || !selector.object.fields) {
return [];
@@ -320,12 +262,12 @@ export class HaObjectSelector extends LitElement {
};
const newItem = await showFormDialog(this, {
title: this._i18n.localize("ui.common.add"),
title: this.hass.localize("ui.common.add"),
schema,
data,
computeLabel: this._computeLabel,
computeHelper: this._computeHelper,
submitText: this._i18n.localize("ui.common.add"),
submitText: this.hass.localize("ui.common.add"),
});
if (newItem === null) {
@@ -348,12 +290,12 @@ export class HaObjectSelector extends LitElement {
const index = ev.currentTarget.index;
const updatedItem = await showFormDialog(this, {
title: this._i18n.localize("ui.common.edit"),
title: this.hass.localize("ui.common.edit"),
schema: this._schema(this.selector),
data: item,
computeLabel: this._computeLabel,
computeHelper: this._computeHelper,
submitText: this._i18n.localize("ui.common.save"),
submitText: this.hass.localize("ui.common.save"),
});
if (updatedItem === null) {
+11 -27
View File
@@ -1,4 +1,3 @@
import type { HassEntities } from "home-assistant-js-websocket";
import { ensureArray } from "../../common/array/ensure-array";
import { createDurationData } from "../../common/datetime/create_duration_data";
import { formatDurationLong } from "../../common/datetime/format_duration";
@@ -6,27 +5,12 @@ import { normalizeDuration } from "../../common/datetime/normalize_duration";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { HomeAssistantFormatters } from "../../types";
import type { AreaRegistryEntry } from "../area/area_registry";
import type { DeviceRegistryEntry } from "../device/device_registry";
import type { FloorRegistryEntry } from "../floor_registry";
import type { HomeAssistant } from "../../types";
import type { Selector } from "../selector";
import { getDurationSelectorMode, getDurationSelectorUnits } from "../selector";
import type { FrontendLocaleData } from "../translation";
export interface FormatSelectorValueOptions {
locale: FrontendLocaleData;
localize: LocalizeFunc;
states: HassEntities;
devices: Record<string, DeviceRegistryEntry>;
areas: Record<string, AreaRegistryEntry>;
floors: Record<string, FloorRegistryEntry>;
formatEntityName: HomeAssistantFormatters["formatEntityName"];
}
export const formatSelectorValue = (
options: FormatSelectorValueOptions,
hass: HomeAssistant,
value: any,
selector?: Selector
) => {
@@ -63,7 +47,7 @@ export const formatSelectorValue = (
return number;
}
return unit_of_measurement
? `${num}${blankBeforeUnit(unit_of_measurement, options.locale)}${unit_of_measurement}`
? `${num}${blankBeforeUnit(unit_of_measurement, hass.locale)}${unit_of_measurement}`
: num.toString();
})
.join(", ");
@@ -73,7 +57,7 @@ export const formatSelectorValue = (
const floors = ensureArray(value);
return floors
.map((floorId) => {
const floor = options.floors[floorId];
const floor = hass.floors[floorId];
if (!floor) {
return floorId;
}
@@ -86,7 +70,7 @@ export const formatSelectorValue = (
const areas = ensureArray(value);
return areas
.map((areaId) => {
const area = options.areas[areaId];
const area = hass.areas[areaId];
if (!area) {
return areaId;
}
@@ -99,11 +83,11 @@ export const formatSelectorValue = (
const entities = ensureArray(value);
return entities
.map((entityId) => {
const stateObj = options.states[entityId];
const stateObj = hass.states[entityId];
if (!stateObj) {
return entityId;
}
const name = options.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
const name = hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME);
return name || entityId;
})
.join(", ");
@@ -113,7 +97,7 @@ export const formatSelectorValue = (
const devices = ensureArray(value);
return devices
.map((deviceId) => {
const device = options.devices[deviceId];
const device = hass.devices[deviceId];
if (!device) {
return deviceId;
}
@@ -141,7 +125,7 @@ export const formatSelectorValue = (
return Object.entries(fields)
.filter(([key]) => key in item && item[key] != null)
.map(([key, field]) =>
formatSelectorValue(options, item[key], field.selector)
formatSelectorValue(hass, item[key], field.selector)
)
.join(" = ");
}
@@ -159,13 +143,13 @@ export const formatSelectorValue = (
data,
getDurationSelectorUnits(selector.duration)
);
const formatted = formatDurationLong(options.locale, duration);
const formatted = formatDurationLong(hass.locale, duration);
const mode = getDurationSelectorMode(selector.duration);
if (!formatted || mode === "positive") {
return formatted;
}
const sign = negative ? "negative" : "positive";
return options.localize(
return hass.localize(
`ui.components.selectors.duration.summary.${mode}_${sign}`,
{ duration: formatted }
);
@@ -481,27 +481,39 @@ export class EntityRegistrySettingsEditor extends LitElement {
return html`
${
!this.hideName && this._device
? html`<ha-row-item>
? html`<ha-row-item class="main-entity">
<span slot="headline"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.use_device_name"
)}
(${computeDeviceNameDisplay(this._device, this.hass.localize)})</span
"ui.dialogs.entity_registry.editor.main_entity",
{
device: computeDeviceNameDisplay(
this._device,
this.hass.localize
),
}
)}</span
>
<span slot="supporting-text"
>${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_settings",
{
link: html`<button
class="link"
@click=${this._openDeviceSettings}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_name_link"
)}
</button>`,
}
)}</span
"ui.dialogs.entity_registry.editor.main_entity_description"
)}
${
this._useDeviceName
? this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_settings",
{
link: html`<button
class="link"
@click=${this._openDeviceSettings}
>
${this.hass.localize(
"ui.dialogs.entity_registry.editor.change_device_name_link"
)}
</button>`,
}
)
: nothing
}</span
>
<ha-switch
slot="end"
@@ -1912,6 +1924,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
margin-inline-start: 0;
}
ha-row-item.main-entity::part(supporting-text),
ha-row-item.device-area::part(supporting-text) {
white-space: normal;
}
+4 -3
View File
@@ -2151,7 +2151,8 @@
"faq": "documentation",
"editor": {
"name": "Name",
"use_device_name": "Use device name",
"main_entity": "Main entity of the device ({device})",
"main_entity_description": "Main entities are shown with the device name only.",
"change_device_name_link": "change the device name",
"restore_name": "Restore default name",
"icon": "Icon",
@@ -2262,8 +2263,8 @@
"note": "Note: This might not work yet with all integrations.",
"use_device_area": "Use device area",
"use_device_area_confirm_title": "Use the area of the device?",
"use_device_area_confirm_text": "This entity is in {area}. Because it has no name of its own, it will use the area of its device instead.",
"use_device_area_required": "An entity without its own name always uses the area of its device. You can {link} in the device settings.",
"use_device_area_confirm_text": "This entity is in {area}. As a main entity, it will use the area of its device instead.",
"use_device_area_required": "A main entity always uses the area of its device. You can {link} in the device settings.",
"change_device_settings": "You can {link} in the device settings",
"change_device_area_link": "change the device area",
"configure_state": "{integration} options",
@@ -1,16 +1,6 @@
import { ContextProvider } from "@lit/context";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
formattersContext,
internationalizationContext,
registriesContext,
statesContext,
} from "../../../src/data/context";
import type { ObjectSelector } from "../../../src/data/selector";
import type {
HomeAssistantFormatters,
HomeAssistantInternationalization,
} from "../../../src/types";
import type { HomeAssistant } from "../../../src/types";
import type { HaObjectSelector } from "../../../src/components/ha-selector/ha-selector-object";
import type {
FormDialogData,
@@ -43,6 +33,16 @@ vi.mock("../../../src/components/ha-yaml-editor", () => {
return {};
});
const hass = {
localize: (key: string) => key,
locale: "en-US",
floors: {},
areas: {},
devices: {},
states: {},
formatEntityName: () => "",
} as unknown as HomeAssistant;
const selectorConfig = {
object: {
multiple: true,
@@ -67,32 +67,13 @@ const mountSelector = async (
value: FormDialogData | FormDialogData[] | "",
config: ObjectSelector = selectorConfig
) => {
const host = document.createElement("div");
new ContextProvider(host, {
context: internationalizationContext,
initialValue: {
localize: (key: string) => key,
locale: { language: "en" },
} as unknown as HomeAssistantInternationalization,
});
new ContextProvider(host, {
context: registriesContext,
initialValue: { entities: {}, devices: {}, areas: {}, floors: {} },
});
new ContextProvider(host, { context: statesContext, initialValue: {} });
new ContextProvider(host, {
context: formattersContext,
initialValue: {
formatEntityName: () => "",
} as unknown as HomeAssistantFormatters,
});
const selector = document.createElement(
"ha-selector-object"
) as HaObjectSelector;
selector.hass = hass;
selector.selector = config;
selector.value = value;
host.append(selector);
document.body.append(host);
document.body.append(selector);
await selector.updateComplete;
return selector;
};
+24 -27
View File
@@ -1,34 +1,33 @@
import { describe, it, expect } from "vitest";
import {
formatSelectorValue,
type FormatSelectorValueOptions,
} from "../../src/data/selector/format_selector_value";
import { formatSelectorValue } from "../../src/data/selector/format_selector_value";
import type { HomeAssistant } from "../../src/types";
// These selectors read none of the options.
const options = {} as FormatSelectorValueOptions;
// formatSelectorValue only touches hass for floor/area/entity/device
// selectors, none of which these tests exercise.
const hass = {} as HomeAssistant;
describe("formatSelectorValue", () => {
it("returns an empty string for nullish values", () => {
expect(formatSelectorValue(options, null)).toBe("");
expect(formatSelectorValue(options, undefined)).toBe("");
expect(formatSelectorValue(hass, null)).toBe("");
expect(formatSelectorValue(hass, undefined)).toBe("");
});
it("renders a plain text value", () => {
expect(
formatSelectorValue(options, "hello", { text: { type: "text" } })
).toBe("hello");
expect(formatSelectorValue(hass, "hello", { text: { type: "text" } })).toBe(
"hello"
);
});
it("applies prefix and suffix for text selectors", () => {
expect(
formatSelectorValue(options, "5", {
formatSelectorValue(hass, "5", {
text: { type: "text", prefix: "$", suffix: " each" },
})
).toBe("$5 each");
});
it("masks a password text value instead of revealing it", () => {
const result = formatSelectorValue(options, "hunter2", {
const result = formatSelectorValue(hass, "hunter2", {
text: { type: "password" },
});
expect(result).not.toContain("hunter2");
@@ -36,7 +35,7 @@ describe("formatSelectorValue", () => {
});
it("masks every value of a multiple password selector", () => {
const result = formatSelectorValue(options, ["one", "two"], {
const result = formatSelectorValue(hass, ["one", "two"], {
text: { type: "password" },
});
expect(result).not.toContain("one");
@@ -46,7 +45,7 @@ describe("formatSelectorValue", () => {
it("masks a nested password field in an object selector preview", () => {
const result = formatSelectorValue(
options,
hass,
{ username: "admin", password: "hunter2" },
{
object: {
@@ -64,7 +63,7 @@ describe("formatSelectorValue", () => {
});
describe("formatSelectorValue duration selector", () => {
const localizedOptions = {
const localizedHass = {
locale: { language: "en" },
localize: (key: string, values?: Record<string, unknown>) => {
const suffix = key.split("ui.components.selectors.duration.summary.")[1];
@@ -75,12 +74,12 @@ describe("formatSelectorValue duration selector", () => {
signed_positive: `+${values?.duration}`,
}[suffix]!;
},
} as unknown as FormatSelectorValueOptions;
} as unknown as HomeAssistant;
it("formats a positive duration without a sign", () => {
expect(
formatSelectorValue(
localizedOptions,
localizedHass,
{ hours: 1, minutes: 30 },
{ duration: {} }
)
@@ -90,13 +89,13 @@ describe("formatSelectorValue duration selector", () => {
it("formats signed durations with an explicit sign", () => {
expect(
formatSelectorValue(
localizedOptions,
localizedHass,
{ minutes: -30 },
{ duration: { mode: "signed" } }
)
).toBe("-30 minutes");
expect(
formatSelectorValue(localizedOptions, "00:30:00", {
formatSelectorValue(localizedHass, "00:30:00", {
duration: { allow_negative: true },
})
).toBe("+30 minutes");
@@ -105,24 +104,22 @@ describe("formatSelectorValue duration selector", () => {
it("formats offsets as before or after", () => {
expect(
formatSelectorValue(
localizedOptions,
localizedHass,
{ hours: -1, minutes: -30 },
{ duration: { mode: "offset" } }
)
).toBe("1 hour, 30 minutes before");
expect(
formatSelectorValue(localizedOptions, "-00:10:00", {
formatSelectorValue(localizedHass, "-00:10:00", {
duration: { mode: "offset" },
})
).toBe("10 minutes before");
expect(
formatSelectorValue(localizedOptions, 45, {
duration: { mode: "offset" },
})
formatSelectorValue(localizedHass, 45, { duration: { mode: "offset" } })
).toBe("45 seconds after");
expect(
formatSelectorValue(
localizedOptions,
localizedHass,
{ minutes: -5 },
{ duration: { mode: "offset" } }
)
@@ -132,7 +129,7 @@ describe("formatSelectorValue duration selector", () => {
it("returns an empty string for a zero offset", () => {
expect(
formatSelectorValue(
localizedOptions,
localizedHass,
{ hours: 0, minutes: 0, seconds: 0 },
{ duration: { mode: "offset" } }
)