Migrate light card, plant card, thermostat card and weather card

This commit is contained in:
Paul Bottein
2025-10-09 11:54:08 +02:00
parent 48266d58fc
commit e73db6f6be
12 changed files with 158 additions and 32 deletions

View File

@@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -15,6 +14,7 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity";
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { computeCardEntityName } from "../common/entity/compute-card-entity-name";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
@@ -133,7 +133,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
`;
}
const name = this._config!.name || computeStateName(stateObj);
const name = computeCardEntityName(this.hass, stateObj, this._config.name);
const color = stateColorCss(stateObj);

View File

@@ -7,7 +7,6 @@ import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorBrightness } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -18,6 +17,7 @@ import { lightSupportsBrightness } from "../../../data/light";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeCardEntityName } from "../common/entity/compute-card-entity-name";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
@@ -92,7 +92,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
((stateObj.attributes.brightness || 0) / 255) * 100
);
const name = this._config.name ?? computeStateName(stateObj);
const name = computeCardEntityName(this.hass, stateObj, this._config.name);
return html`
<ha-card>

View File

@@ -11,11 +11,11 @@ import { customElement, property, state } from "lit/decorators";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { batteryLevelIcon } from "../../../common/entity/battery_icon";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-card";
import "../../../components/ha-svg-icon";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeCardEntityName } from "../common/entity/compute-card-entity-name";
import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
@@ -119,7 +119,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
style="background-image:url(${stateObj.attributes.entity_picture})"
>
<div class="header">
${this._config.name || computeStateName(stateObj)}
${computeCardEntityName(this.hass, stateObj, this._config.name)}
</div>
</div>
<div class="content">

View File

@@ -7,7 +7,6 @@ import { styleMap } from "lit/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -16,6 +15,7 @@ import "../../../state-control/water_heater/ha-state-control-water_heater-temper
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
import type { LovelaceCardFeatureContext } from "../card-features/types";
import { computeCardEntityName } from "../common/entity/compute-card-entity-name";
import { findEntities } from "../common/find-entities";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type {
@@ -132,7 +132,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
}
const domain = computeDomain(stateObj.entity_id);
const name = this._config!.name || computeStateName(stateObj);
const name = computeCardEntityName(this.hass, stateObj, this._config.name);
const color = stateColorCss(stateObj);

View File

@@ -7,7 +7,6 @@ import { classMap } from "lit/directives/class-map";
import { formatDateWeekdayShort } from "../../../common/datetime/format_date";
import { formatTime } from "../../../common/datetime/format_time";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { formatNumber } from "../../../common/number/format_number";
import "../../../components/ha-card";
@@ -27,6 +26,7 @@ import {
} from "../../../data/weather";
import type { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { computeCardEntityName } from "../common/entity/compute-card-entity-name";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
@@ -229,7 +229,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return html`
<ha-card class="unavailable" @click=${this._handleAction}>
${this.hass.localize("ui.panel.lovelace.warning.entity_unavailable", {
entity: `${computeStateName(stateObj)} (${this._config.entity})`,
entity: `${computeCardEntityName(this.hass, stateObj, this._config.name)} (${this._config.entity})`,
})}
</ha-card>
`;
@@ -260,7 +260,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
const dayNight = forecastData?.type === "twice_daily";
const weatherStateIcon = getWeatherStateIcon(stateObj.state, this);
const name = this._config.name ?? computeStateName(stateObj);
const name = computeCardEntityName(this.hass, stateObj, this._config.name);
return html`
<ha-card

View File

@@ -305,7 +305,7 @@ export interface GlanceCardConfig extends LovelaceCardConfig {
export interface HumidifierCardConfig extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
show_current_as_primary?: boolean;
features?: LovelaceCardFeatureConfig[];
}
@@ -321,7 +321,7 @@ export interface IframeCardConfig extends LovelaceCardConfig {
export interface LightCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
theme?: string;
icon?: string;
tap_action?: ActionConfig;
@@ -508,7 +508,7 @@ export interface PlantAttributeTarget extends EventTarget {
}
export interface PlantStatusCardConfig extends LovelaceCardConfig {
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
entity: string;
theme?: string;
}
@@ -550,14 +550,14 @@ export interface GridCardConfig extends StackCardConfig {
export interface ThermostatCardConfig extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
show_current_as_primary?: boolean;
features?: LovelaceCardFeatureConfig[];
}
export interface WeatherForecastCardConfig extends LovelaceCardConfig {
entity: string;
name?: string;
name?: string | EntityNameItem | EntityNameItem[];
show_current?: boolean;
show_forecast?: boolean;
forecast_type?: ForecastType;

View File

@@ -14,6 +14,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } 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-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -29,6 +30,7 @@ import type {
import type { HumidifierCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
@@ -43,7 +45,7 @@ const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
show_current_as_primary: optional(boolean()),
features: optional(array(any())),
@@ -56,13 +58,19 @@ const SCHEMA = [
required: true,
selector: { entity: { domain: "humidifier" } },
},
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
schema: [{ name: "theme", selector: { theme: {} } }],
},
{
name: "show_current_as_primary",

View File

@@ -4,6 +4,7 @@ import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { mdiGestureTap } from "@mdi/js";
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 { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
@@ -11,12 +12,13 @@ import type { LightCardConfig } 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({
name: optional(string()),
name: optional(entityNameStruct),
entity: optional(string()),
theme: optional(string()),
icon: optional(string()),
@@ -32,11 +34,19 @@ const SCHEMA = [
required: true,
selector: { entity: { domain: "light" } },
},
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {

View File

@@ -2,25 +2,35 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, 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 { SchemaUnion } from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { PlantStatusCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
})
);
const SCHEMA = [
{ name: "entity", required: true, selector: { entity: { domain: "plant" } } },
{ name: "name", selector: { text: {} } },
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{ name: "theme", selector: { theme: {} } },
] as const;

View File

@@ -14,6 +14,7 @@ import {
} from "superstruct";
import type { HASSDomEvent } 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-expansion-panel";
import "../../../../components/ha-form/ha-form";
import type {
@@ -29,6 +30,7 @@ import type {
import type { ThermostatCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import "./hui-card-features-editor";
@@ -50,7 +52,7 @@ const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
show_current_as_primary: optional(boolean()),
features: optional(array(any())),
@@ -84,13 +86,19 @@ export class HuiThermostatCardEditor
name: "entity",
selector: { entity: { domain: ["climate", "water_heater"] } },
},
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
type: "grid",
name: "",
schema: [
{ name: "name", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
],
schema: [{ name: "theme", selector: { theme: {} } }],
},
...(domain === "climate"
? [

View File

@@ -12,6 +12,7 @@ import {
string,
} from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display";
import { supportsFeature } from "../../../../common/entity/supports-feature";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -24,12 +25,13 @@ import type { WeatherForecastCardConfig } 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";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(string()),
name: optional(entityNameStruct),
theme: optional(string()),
show_current: optional(boolean()),
show_forecast: optional(boolean()),
@@ -148,7 +150,15 @@ export class HuiWeatherForecastCardEditor
required: true,
selector: { entity: { domain: "weather" } },
},
{ name: "name", selector: { text: {} } },
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_ENTITY_NAME,
},
},
context: { entity: "entity" },
},
{
name: "",
type: "grid",

View File

@@ -0,0 +1,80 @@
import { describe, expect, it, vi } from "vitest";
import { DEFAULT_ENTITY_NAME } from "../../../../../src/common/entity/compute_entity_name_display";
import { computeCardEntityName } from "../../../../../src/panels/lovelace/common/entity/compute-card-entity-name";
import type { HomeAssistant } from "../../../../../src/types";
import { mockStateObj } from "../../../../common/entity/context/context-mock";
const createMockHass = (
mockFormatEntityName: ReturnType<typeof vi.fn>
): HomeAssistant =>
({
formatEntityName: mockFormatEntityName,
}) as unknown as HomeAssistant;
describe("computeCardEntityName", () => {
it("returns the string directly when nameConfig is a string", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const result = computeCardEntityName(hass, stateObj, "Custom Name");
expect(result).toBe("Custom Name");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("returns empty string when nameConfig is empty string", () => {
const mockFormatEntityName = vi.fn();
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const result = computeCardEntityName(hass, stateObj, "");
expect(result).toBe("");
expect(mockFormatEntityName).not.toHaveBeenCalled();
});
it("calls formatEntityName with DEFAULT_ENTITY_NAME when nameConfig is undefined", () => {
const mockFormatEntityName = vi.fn(() => "Formatted Name");
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const result = computeCardEntityName(hass, stateObj, undefined);
expect(result).toBe("Formatted Name");
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(
stateObj,
DEFAULT_ENTITY_NAME
);
});
it("calls formatEntityName with EntityNameItem config", () => {
const mockFormatEntityName = vi.fn(() => "Formatted Name");
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.bedroom" });
const nameConfig = { type: "device" as const };
const result = computeCardEntityName(hass, stateObj, nameConfig);
expect(result).toBe("Formatted Name");
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig);
});
it("calls formatEntityName with array of EntityNameItems", () => {
const mockFormatEntityName = vi.fn(() => "Formatted Name");
const hass = createMockHass(mockFormatEntityName);
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const nameConfig = [
{ type: "device" as const },
{ type: "entity" as const },
];
const result = computeCardEntityName(hass, stateObj, nameConfig);
expect(result).toBe("Formatted Name");
expect(mockFormatEntityName).toHaveBeenCalledTimes(1);
expect(mockFormatEntityName).toHaveBeenCalledWith(stateObj, nameConfig);
});
});