1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-24 12:49:19 +00:00

Use entity naming in more cards and badges (#27541)

* Add support for button card, glance card and entities card

* Add tests

* Add support for attribute and button row

* Add support to heading badge

* Undo changes from rows

* Add comment
This commit is contained in:
Paul Bottein
2025-10-19 14:08:28 +02:00
committed by GitHub
parent ef0da0a7ee
commit 8a42d15bde
10 changed files with 141 additions and 26 deletions

View File

@@ -20,6 +20,7 @@ import "../chips/ha-chip-set";
import "../chips/ha-input-chip"; import "../chips/ha-input-chip";
import "../ha-combo-box"; import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box";
import "../ha-input-helper-text";
import "../ha-sortable"; import "../ha-sortable";
interface EntityNameOption { interface EntityNameOption {
@@ -239,7 +240,6 @@ export class HaEntityNamePicker extends LitElement {
.autofocus=${this.autofocus} .autofocus=${this.autofocus}
.disabled=${this.disabled} .disabled=${this.disabled}
.required=${this.required && !value.length} .required=${this.required && !value.length}
.helper=${this.helper}
.items=${options} .items=${options}
allow-custom-value allow-custom-value
item-id-path="value" item-id-path="value"
@@ -253,9 +253,20 @@ export class HaEntityNamePicker extends LitElement {
</ha-combo-box> </ha-combo-box>
</mwc-menu-surface> </mwc-menu-surface>
</div> </div>
${this._renderHelper()}
`; `;
} }
private _renderHelper() {
return this.helper
? html`
<ha-input-helper-text .disabled=${this.disabled}>
${this.helper}
</ha-input-helper-text>
`
: nothing;
}
private _onClosed(ev) { private _onClosed(ev) {
ev.stopPropagation(); ev.stopPropagation();
this._opened = false; this._opened = false;
@@ -510,6 +521,11 @@ export class HaEntityNamePicker extends LitElement {
.sortable-drag { .sortable-drag {
cursor: grabbing; cursor: grabbing;
} }
ha-input-helper-text {
display: block;
margin: var(--ha-space-2) 0 0;
}
`; `;
} }

View File

@@ -179,7 +179,7 @@ export class HaGenericPicker extends LitElement {
} }
ha-input-helper-text { ha-input-helper-text {
display: block; display: block;
margin: 8px 0 0; margin: var(--ha-space-2) 0 0;
} }
`, `,
]; ];

View File

@@ -9,13 +9,14 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators"; import { customElement, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import { DOMAINS_TOGGLE } from "../../../common/const"; import { DOMAINS_TOGGLE } from "../../../common/const";
import { transform } from "../../../common/decorators/transform"; import { transform } from "../../../common/decorators/transform";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { stateActive } from "../../../common/entity/state_active";
import { import {
stateColorBrightness, stateColorBrightness,
stateColorCss, stateColorCss,
@@ -40,6 +41,7 @@ import type { FrontendLocaleData } from "../../../data/translation";
import type { Themes } from "../../../data/ws-themes"; import type { Themes } from "../../../data/ws-themes";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import { createEntityNotFoundWarning } from "../components/hui-warning"; import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -49,8 +51,6 @@ import type {
LovelaceGridOptions, LovelaceGridOptions,
} from "../types"; } from "../types";
import type { ButtonCardConfig } from "./types"; import type { ButtonCardConfig } from "./types";
import { computeCssColor } from "../../../common/color/compute-color";
import { stateActive } from "../../../common/entity/state_active";
export const getEntityDefaultButtonAction = (entityId?: string) => export const getEntityDefaultButtonAction = (entityId?: string) =>
entityId && DOMAINS_TOGGLE.has(computeDomain(entityId)) entityId && DOMAINS_TOGGLE.has(computeDomain(entityId))
@@ -183,9 +183,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
`; `;
} }
const name = this._config.show_name const name = computeLovelaceEntityName(
? this._config.name || (stateObj ? computeStateName(stateObj) : "") this.hass,
: ""; stateObj,
this._config.name
);
return html` return html`
<ha-card <ha-card
@@ -195,8 +197,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
hasDoubleClick: hasAction(this._config!.double_tap_action), hasDoubleClick: hasAction(this._config!.double_tap_action),
})} })}
role="button" role="button"
aria-label=${this._config.name || aria-label=${name}
(stateObj ? computeStateName(stateObj) : "")}
tabindex=${ifDefined( tabindex=${ifDefined(
hasAction(this._config.tap_action) ? "0" : undefined hasAction(this._config.tap_action) ? "0" : undefined
)} )}

View File

@@ -272,7 +272,6 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj} .stateObj=${stateObj}
.hass=${this.hass} .hass=${this.hass}
.content=${this._config.state_content} .content=${this._config.state_content}
.name=${name}
> >
</state-display> </state-display>
`; `;

View File

@@ -4,6 +4,7 @@ import {
type EntityNameItem, type EntityNameItem,
} from "../../../../common/entity/compute_entity_name_display"; } from "../../../../common/entity/compute_entity_name_display";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { ensureArray } from "../../../../common/array/ensure-array";
/** /**
* Computes the display name for an entity in Lovelace (cards and badges). * Computes the display name for an entity in Lovelace (cards and badges).
@@ -15,9 +16,24 @@ import type { HomeAssistant } from "../../../../types";
*/ */
export const computeLovelaceEntityName = ( export const computeLovelaceEntityName = (
hass: HomeAssistant, hass: HomeAssistant,
stateObj: HassEntity, stateObj: HassEntity | undefined,
nameConfig: string | EntityNameItem | EntityNameItem[] | undefined nameConfig: string | EntityNameItem | EntityNameItem[] | undefined
): string => ): string => {
typeof nameConfig === "string" if (typeof nameConfig === "string") {
? nameConfig return nameConfig;
: hass.formatEntityName(stateObj, nameConfig || DEFAULT_ENTITY_NAME); }
const config = nameConfig || DEFAULT_ENTITY_NAME;
if (stateObj) {
return hass.formatEntityName(stateObj, config);
}
// If entity is not found, fall back to text parts in config
// This allows for static names even when the entity is missing
// e.g. for a card that doesn't require an entity
const textParts = ensureArray(config)
.filter((item) => item.type === "text")
.map((item) => ("text" in item ? item.text : ""));
if (textParts.length) {
return textParts.join(" ");
}
return "";
};

View File

@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct"; import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { import type {
HaFormSchema, HaFormSchema,
@@ -16,13 +17,14 @@ import type { ButtonCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import { configElementStyle } from "./config-elements-style"; import { configElementStyle } from "./config-elements-style";
const cardConfigStruct = assign( const cardConfigStruct = assign(
baseLovelaceCardConfig, baseLovelaceCardConfig,
object({ object({
entity: optional(string()), entity: optional(string()),
name: optional(string()), name: optional(entityNameStruct),
show_name: optional(boolean()), show_name: optional(boolean()),
icon: optional(string()), icon: optional(string()),
show_icon: optional(boolean()), show_icon: optional(boolean()),
@@ -68,7 +70,13 @@ export class HuiButtonCardEditor
(entityId: string | undefined) => (entityId: string | undefined) =>
[ [
{ name: "entity", selector: { entity: {} } }, { name: "entity", selector: { entity: {} } },
{ name: "name", selector: { text: {} } }, {
name: "name",
selector: {
entity_name: { default_name: DEFAULT_ENTITY_NAME },
},
context: { entity: "entity" },
},
{ {
name: "", name: "",
type: "grid", type: "grid",

View File

@@ -13,6 +13,7 @@ import {
union, union,
} from "superstruct"; } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize"; import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-expansion-panel"; import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
@@ -27,6 +28,7 @@ import type { LovelaceGenericElementEditor } from "../../types";
import "../conditions/ha-card-conditions-editor"; import "../conditions/ha-card-conditions-editor";
import { configElementStyle } from "../config-elements/config-elements-style"; import { configElementStyle } from "../config-elements/config-elements-style";
import { actionConfigStruct } from "../structs/action-struct"; import { actionConfigStruct } from "../structs/action-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = { export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
type: "entity", type: "entity",
@@ -37,7 +39,7 @@ export const DEFAULT_CONFIG: Partial<EntityHeadingBadgeConfig> = {
const entityConfigStruct = object({ const entityConfigStruct = object({
type: optional(string()), type: optional(string()),
entity: optional(string()), entity: optional(string()),
name: optional(string()), name: optional(entityNameStruct),
icon: optional(string()), icon: optional(string()),
state_content: optional(union([string(), array(string())])), state_content: optional(union([string(), array(string())])),
show_state: optional(boolean()), show_state: optional(boolean()),
@@ -92,8 +94,11 @@ export class HuiHeadingEntityEditor
{ {
name: "name", name: "name",
selector: { selector: {
text: {}, entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
}, },
context: { entity: "entity" },
}, },
{ {
name: "icon", name: "icon",

View File

@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color"; import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-heading-badge"; import "../../../components/ha-heading-badge";
@@ -16,6 +15,7 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import "../../../state-display/state-display"; import "../../../state-display/state-display";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name";
import { handleAction } from "../common/handle-action"; import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action"; import { hasAction } from "../common/has-action";
import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor"; import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor";
@@ -137,7 +137,11 @@ export class HuiEntityHeadingBadge
"--icon-color": color, "--icon-color": color,
}; };
const name = config.name || computeStateName(stateObj); const name = computeLovelaceEntityName(
this.hass,
stateObj,
this._config.name
);
return html` return html`
<ha-heading-badge <ha-heading-badge
@@ -166,7 +170,7 @@ export class HuiEntityHeadingBadge
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .stateObj=${stateObj}
.content=${config.state_content} .content=${config.state_content}
.name=${config.name} .name=${name}
dash-unavailable dash-unavailable
></state-display> ></state-display>
` `

View File

@@ -5,7 +5,6 @@ import { customElement, property } from "lit/decorators";
import { join } from "lit/directives/join"; import { join } from "lit/directives/join";
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import "../components/ha-relative-time"; import "../components/ha-relative-time";
import { isUnavailableState } from "../data/entity"; import { isUnavailableState } from "../data/entity";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor";
@@ -100,8 +99,8 @@ class StateDisplay extends LitElement {
return this.hass!.formatEntityState(stateObj); return this.hass!.formatEntityState(stateObj);
} }
if (content === "name") { if (content === "name" && this.name) {
return html`${this.name || computeStateName(stateObj)}`; return html`${this.name}`;
} }
let relativeDateTime: string | Date | undefined; let relativeDateTime: string | Date | undefined;

View File

@@ -77,4 +77,71 @@ describe("computeLovelaceEntityName", () => {
expect(mockFormatEntityName).toHaveBeenCalledTimes(1); expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig); expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig);
}); });
describe("when stateObj is undefined", () => {
it("returns empty string when nameConfig is undefined", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const result = computeLovelaceEntityName(hass, undefined, undefined);
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns text from single text EntityNameItem", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = { type: "text" as const, text: "Custom Text" };
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("Custom Text");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns joined text from multiple text EntityNameItems", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "text" as const, text: "First" },
{ type: "text" as const, text: "Second" },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("First Second");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns only text items when mixed with non-text items", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "text" as const, text: "Prefix" },
{ type: "device" as const },
{ type: "text" as const, text: "Suffix" },
{ type: "entity" as const },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("Prefix Suffix");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns empty string when no text items in config", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const nameConfig = [
{ type: "device" as const },
{ type: "entity" as const },
];
const result = computeLovelaceEntityName(hass, undefined, nameConfig);
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
});
}); });