diff --git a/src/components/entity/ha-entity-name-picker.ts b/src/components/entity/ha-entity-name-picker.ts index 4899d130c8..43c8290d21 100644 --- a/src/components/entity/ha-entity-name-picker.ts +++ b/src/components/entity/ha-entity-name-picker.ts @@ -20,6 +20,7 @@ import "../chips/ha-chip-set"; import "../chips/ha-input-chip"; import "../ha-combo-box"; import type { HaComboBox } from "../ha-combo-box"; +import "../ha-input-helper-text"; import "../ha-sortable"; interface EntityNameOption { @@ -239,7 +240,6 @@ export class HaEntityNamePicker extends LitElement { .autofocus=${this.autofocus} .disabled=${this.disabled} .required=${this.required && !value.length} - .helper=${this.helper} .items=${options} allow-custom-value item-id-path="value" @@ -253,9 +253,20 @@ export class HaEntityNamePicker extends LitElement { + ${this._renderHelper()} `; } + private _renderHelper() { + return this.helper + ? html` + + ${this.helper} + + ` + : nothing; + } + private _onClosed(ev) { ev.stopPropagation(); this._opened = false; @@ -510,6 +521,11 @@ export class HaEntityNamePicker extends LitElement { .sortable-drag { cursor: grabbing; } + + ha-input-helper-text { + display: block; + margin: var(--ha-space-2) 0 0; + } `; } diff --git a/src/components/ha-generic-picker.ts b/src/components/ha-generic-picker.ts index 154e8cb869..0e2e8d5f25 100644 --- a/src/components/ha-generic-picker.ts +++ b/src/components/ha-generic-picker.ts @@ -179,7 +179,7 @@ export class HaGenericPicker extends LitElement { } ha-input-helper-text { display: block; - margin: 8px 0 0; + margin: var(--ha-space-2) 0 0; } `, ]; diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts index ee32262d04..90804ba3aa 100644 --- a/src/panels/lovelace/cards/hui-button-card.ts +++ b/src/panels/lovelace/cards/hui-button-card.ts @@ -9,13 +9,14 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import { styleMap } from "lit/directives/style-map"; +import { computeCssColor } from "../../../common/color/compute-color"; import { DOMAINS_TOGGLE } from "../../../common/const"; import { transform } from "../../../common/decorators/transform"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { fireEvent } from "../../../common/dom/fire_event"; import { computeDomain } from "../../../common/entity/compute_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 { stateColorBrightness, stateColorCss, @@ -40,6 +41,7 @@ import type { FrontendLocaleData } from "../../../data/translation"; import type { Themes } from "../../../data/ws-themes"; import type { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { findEntities } from "../common/find-entities"; import { hasAction } from "../common/has-action"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -49,8 +51,6 @@ import type { LovelaceGridOptions, } 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) => entityId && DOMAINS_TOGGLE.has(computeDomain(entityId)) @@ -183,9 +183,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { `; } - const name = this._config.show_name - ? this._config.name || (stateObj ? computeStateName(stateObj) : "") - : ""; + const name = computeLovelaceEntityName( + this.hass, + stateObj, + this._config.name + ); return html` `; diff --git a/src/panels/lovelace/common/entity/compute-lovelace-entity-name.ts b/src/panels/lovelace/common/entity/compute-lovelace-entity-name.ts index 4a336ee2ea..53ebd2df05 100644 --- a/src/panels/lovelace/common/entity/compute-lovelace-entity-name.ts +++ b/src/panels/lovelace/common/entity/compute-lovelace-entity-name.ts @@ -4,6 +4,7 @@ import { type EntityNameItem, } from "../../../../common/entity/compute_entity_name_display"; import type { HomeAssistant } from "../../../../types"; +import { ensureArray } from "../../../../common/array/ensure-array"; /** * Computes the display name for an entity in Lovelace (cards and badges). @@ -15,9 +16,24 @@ import type { HomeAssistant } from "../../../../types"; */ export const computeLovelaceEntityName = ( hass: HomeAssistant, - stateObj: HassEntity, + stateObj: HassEntity | undefined, nameConfig: string | EntityNameItem | EntityNameItem[] | undefined -): string => - typeof nameConfig === "string" - ? nameConfig - : hass.formatEntityName(stateObj, nameConfig || DEFAULT_ENTITY_NAME); +): string => { + if (typeof nameConfig === "string") { + return nameConfig; + } + 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 ""; +}; diff --git a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts index b77ad77e50..40b6a15797 100644 --- a/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-button-card-editor.ts @@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { assert, assign, boolean, object, optional, string } from "superstruct"; 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 type { HaFormSchema, @@ -16,13 +17,14 @@ import type { ButtonCardConfig } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; import { actionConfigStruct } from "../structs/action-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; +import { entityNameStruct } from "../structs/entity-name-struct"; import { configElementStyle } from "./config-elements-style"; const cardConfigStruct = assign( baseLovelaceCardConfig, object({ entity: optional(string()), - name: optional(string()), + name: optional(entityNameStruct), show_name: optional(boolean()), icon: optional(string()), show_icon: optional(boolean()), @@ -68,7 +70,13 @@ export class HuiButtonCardEditor (entityId: string | undefined) => [ { name: "entity", selector: { entity: {} } }, - { name: "name", selector: { text: {} } }, + { + name: "name", + selector: { + entity_name: { default_name: DEFAULT_ENTITY_NAME }, + }, + context: { entity: "entity" }, + }, { name: "", type: "grid", diff --git a/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts index 19c607d6f1..dfc733c588 100644 --- a/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts +++ b/src/panels/lovelace/editor/heading-badge-editor/hui-entity-heading-badge-editor.ts @@ -13,6 +13,7 @@ import { union, } from "superstruct"; 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 "../../../../components/ha-expansion-panel"; import "../../../../components/ha-form/ha-form"; @@ -27,6 +28,7 @@ import type { LovelaceGenericElementEditor } from "../../types"; import "../conditions/ha-card-conditions-editor"; import { configElementStyle } from "../config-elements/config-elements-style"; import { actionConfigStruct } from "../structs/action-struct"; +import { entityNameStruct } from "../structs/entity-name-struct"; export const DEFAULT_CONFIG: Partial = { type: "entity", @@ -37,7 +39,7 @@ export const DEFAULT_CONFIG: Partial = { const entityConfigStruct = object({ type: optional(string()), entity: optional(string()), - name: optional(string()), + name: optional(entityNameStruct), icon: optional(string()), state_content: optional(union([string(), array(string())])), show_state: optional(boolean()), @@ -92,8 +94,11 @@ export class HuiHeadingEntityEditor { name: "name", selector: { - text: {}, + entity_name: { + default_name: DEFAULT_ENTITY_NAME, + }, }, + context: { entity: "entity" }, }, { name: "icon", diff --git a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts index 7779e3a6e2..34b67982e8 100644 --- a/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts +++ b/src/panels/lovelace/heading-badges/hui-entity-heading-badge.ts @@ -7,7 +7,6 @@ import memoizeOne from "memoize-one"; import { computeCssColor } from "../../../common/color/compute-color"; import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; import { computeDomain } from "../../../common/entity/compute_domain"; -import { computeStateName } from "../../../common/entity/compute_state_name"; import { stateActive } from "../../../common/entity/state_active"; import { stateColorCss } from "../../../common/entity/state_color"; import "../../../components/ha-heading-badge"; @@ -16,6 +15,7 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; import "../../../state-display/state-display"; import type { HomeAssistant } from "../../../types"; import { actionHandler } from "../common/directives/action-handler-directive"; +import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; import { handleAction } from "../common/handle-action"; import { hasAction } from "../common/has-action"; import { DEFAULT_CONFIG } from "../editor/heading-badge-editor/hui-entity-heading-badge-editor"; @@ -137,7 +137,11 @@ export class HuiEntityHeadingBadge "--icon-color": color, }; - const name = config.name || computeStateName(stateObj); + const name = computeLovelaceEntityName( + this.hass, + stateObj, + this._config.name + ); return html` ` diff --git a/src/state-display/state-display.ts b/src/state-display/state-display.ts index 0a2fbbe685..a112024d0a 100644 --- a/src/state-display/state-display.ts +++ b/src/state-display/state-display.ts @@ -5,7 +5,6 @@ import { customElement, property } from "lit/decorators"; import { join } from "lit/directives/join"; import { ensureArray } from "../common/array/ensure-array"; import { computeStateDomain } from "../common/entity/compute_state_domain"; -import { computeStateName } from "../common/entity/compute_state_name"; import "../components/ha-relative-time"; import { isUnavailableState } from "../data/entity"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../data/sensor"; @@ -100,8 +99,8 @@ class StateDisplay extends LitElement { return this.hass!.formatEntityState(stateObj); } - if (content === "name") { - return html`${this.name || computeStateName(stateObj)}`; + if (content === "name" && this.name) { + return html`${this.name}`; } let relativeDateTime: string | Date | undefined; diff --git a/test/panels/lovelace/common/entity/compute-lovelace-entity-name.test.ts b/test/panels/lovelace/common/entity/compute-lovelace-entity-name.test.ts index 3e9028bef3..77e4291a8a 100644 --- a/test/panels/lovelace/common/entity/compute-lovelace-entity-name.test.ts +++ b/test/panels/lovelace/common/entity/compute-lovelace-entity-name.test.ts @@ -77,4 +77,71 @@ describe("computeLovelaceEntityName", () => { expect(mockFormatEntityName).toHaveBeenCalledTimes(1); 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(); + }); + }); });