From 5b981512655d06fbf5d05f4ff28243c885e2f937 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 16 Jul 2026 15:15:15 +0200 Subject: [PATCH 1/2] Migrate entities and glance card state_color to color --- src/components/entity/state-badge.ts | 35 +++-- src/components/entity/state-info.ts | 6 +- .../lovelace/cards/hui-entities-card.ts | 27 ++-- src/panels/lovelace/cards/hui-glance-card.ts | 20 ++- .../lovelace/cards/migrate-card-config.ts | 77 ++++++---- src/panels/lovelace/cards/types.ts | 8 + .../lovelace/common/entity-color-config.ts | 34 +++++ .../common/entity-time-format-config.ts | 27 ++++ .../components/hui-generic-entity-row.ts | 1 + .../hui-entities-card-editor.ts | 144 +++++++++--------- .../hui-generic-entity-row-editor.ts | 29 +++- .../config-elements/hui-glance-card-editor.ts | 22 ++- .../editor/structs/entities-struct.ts | 2 +- .../special-rows/hui-conditional-row.ts | 18 +-- .../cards/migrate-card-config.test.ts | 86 +++++++++++ .../common/entity-color-config.test.ts | 67 ++++++++ 16 files changed, 443 insertions(+), 160 deletions(-) create mode 100644 src/panels/lovelace/common/entity-color-config.ts create mode 100644 src/panels/lovelace/common/entity-time-format-config.ts create mode 100644 test/panels/lovelace/common/entity-color-config.test.ts diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts index b65459b66789..7377f90869f0 100644 --- a/src/components/entity/state-badge.ts +++ b/src/components/entity/state-badge.ts @@ -6,8 +6,10 @@ import { LitElement, css, html, nothing } from "lit"; import { customElement, property, 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 { computeDomain } from "../../common/entity/compute_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain"; +import { stateActive } from "../../common/entity/state_active"; import { stateColorBrightness, stateColorCss, @@ -27,10 +29,15 @@ export class StateBadge extends LitElement { @property({ attribute: false }) public overrideImage?: string; - // Cannot be a boolean attribute because undefined is treated different than - // false. When it is undefined, state is still colored for light entities. + /** + * Cannot be a boolean attribute because undefined is treated different than + * false. When it is undefined, state is still colored for light entities. + * @deprecated use `color` instead + */ @property({ attribute: false }) public stateColor?: boolean; + // "state", "none" or a color (theme color name or CSS color). Custom colors + // apply when the entity is active. Takes precedence over stateColor. @property() public color?: string; // @todo Consider reworking to eliminate need for attribute since it is manipulated internally @@ -67,11 +74,17 @@ export class StateBadge extends LitElement { } } - private get _stateColor() { + private get _color(): string | undefined { + if (this.color) { + return this.color; + } + if (this.stateColor !== undefined) { + return this.stateColor ? "state" : "none"; + } const domain = this.stateObj ? computeStateDomain(this.stateObj) : undefined; - return this.stateColor ?? domain === "light"; + return domain === "light" ? "state" : undefined; } protected render() { @@ -131,6 +144,7 @@ export class StateBadge extends LitElement { if (stateObj) { const domain = computeDomain(stateObj.entity_id); + const color = this._color; if (this.overrideImage === undefined) { // hide icon if we have entity picture if ( @@ -147,13 +161,10 @@ export class StateBadge extends LitElement { } backgroundImage = `url(${imageUrl})`; this.icon = false; - } else if (this.color) { - // Externally provided overriding color wins over state color - iconStyle.color = this.color; - } else if (this._stateColor) { - const color = stateColorCss(stateObj); - if (color) { - iconStyle.color = color; + } else if (color === "state") { + const stateColor = stateColorCss(stateObj); + if (stateColor) { + iconStyle.color = stateColor; } if (stateObj.attributes.rgb_color) { iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`; @@ -180,6 +191,8 @@ export class StateBadge extends LitElement { delete iconStyle.color; } } + } else if (color && color !== "none" && stateActive(stateObj)) { + iconStyle.color = computeCssColor(color); } } else if (this.overrideImage) { backgroundImage = `url(${this._resolveImageUrl(this.overrideImage)})`; diff --git a/src/components/entity/state-info.ts b/src/components/entity/state-info.ts index df3e457175d9..16e93c1244fa 100644 --- a/src/components/entity/state-info.ts +++ b/src/components/entity/state-info.ts @@ -1,6 +1,7 @@ import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; import type { HomeAssistant } from "../../types"; import "../ha-relative-time"; import "../ha-tooltip"; @@ -23,10 +24,11 @@ class StateInfo extends LitElement { const name = this.hass.formatEntityName(this.stateObj, { type: "entity" }); + // Inline style because the state-badge color API only colors active entities return html`
diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts index ed1916e6ab94..9aa1964e55cb 100644 --- a/src/panels/lovelace/cards/hui-entities-card.ts +++ b/src/panels/lovelace/cards/hui-entities-card.ts @@ -8,6 +8,7 @@ import "../../../components/ha-card"; import type { HomeAssistant } from "../../../types"; import { computeCardSize } from "../common/compute-card-size"; import { findEntities } from "../common/find-entities"; +import { applyDefaultColor } from "../common/entity-color-config"; import { processConfigEntities } from "../common/process-config-entities"; import "../components/hui-entities-toggle"; import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; @@ -24,7 +25,7 @@ import type { LovelaceHeaderFooter, } from "../types"; import { migrateEntitiesCardConfig } from "./migrate-card-config"; -import type { EntitiesCardConfig } from "./types"; +import type { EntitiesCardConfig, EntitiesCardEntityConfig } from "./types"; import { haStyleScrollbar } from "../../../resources/styles"; export const computeShowHeaderToggle = < @@ -159,7 +160,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { const migratedConfig = migrateEntitiesCardConfig(config); const entities = processConfigEntities(migratedConfig.entities); - this._config = migratedConfig; + this._config = { color: "state", ...migratedConfig }; this._configEntities = entities; this._showHeaderToggle = computeShowHeaderToggle(migratedConfig, entities); if (this._config.header) { @@ -343,18 +344,18 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard { `, ]; - private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult { - const element = createRowElement( - (!("type" in entityConf) || entityConf.type === "conditional") && - "state_color" in this._config! - ? ({ - state_color: this._config.state_color, - ...(entityConf as EntityConfig), - } as EntityConfig) - : entityConf.type === "perform-action" - ? { ...entityConf, type: "call-service" } - : entityConf + private _rowConfig(entityConf: LovelaceRowConfig): LovelaceRowConfig { + if (entityConf.type === "perform-action") { + return { ...entityConf, type: "call-service" }; + } + return applyDefaultColor( + entityConf as EntitiesCardEntityConfig, + this._config!.color ); + } + + private _renderEntity(entityConf: LovelaceRowConfig): TemplateResult { + const element = createRowElement(this._rowConfig(entityConf)); if (this._hass) { element.hass = this._hass; } diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index 7fabdfbb2a6a..d091ab70a096 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -26,6 +26,7 @@ import { findEntities } from "../common/find-entities"; import { handleAction } from "../common/handle-action"; import { hasAction, hasAnyAction } from "../common/has-action"; import { hasConfigOrEntitiesChanged } from "../common/has-changed"; +import { applyDefaultColor } from "../common/entity-color-config"; import { processConfigEntities } from "../common/process-config-entities"; import "../components/hui-timestamp-display"; import { createEntityNotFoundWarning } from "../components/hui-warning"; @@ -87,14 +88,19 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { show_name: true, show_state: true, show_icon: true, - state_color: true, + color: "state", ...migratedConfig, }; + const cardColor = this._config.color; const entities = processConfigEntities(migratedConfig.entities).map( - (entityConf) => ({ - hold_action: { action: "more-info" } as MoreInfoActionConfig, - ...entityConf, - }) + (entityConf) => + applyDefaultColor( + { + hold_action: { action: "more-info" } as MoreInfoActionConfig, + ...entityConf, + }, + cardColor + ) ); for (const entity of entities) { @@ -294,9 +300,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { .stateObj=${stateObj} .overrideIcon=${entityConf.icon} .overrideImage=${entityConf.image} - .stateColor=${ - entityConf.state_color ?? this._config!.state_color - } + .color=${entityConf.color} > ` : "" diff --git a/src/panels/lovelace/cards/migrate-card-config.ts b/src/panels/lovelace/cards/migrate-card-config.ts index 50c4b6f7e7d3..8221033c391f 100644 --- a/src/panels/lovelace/cards/migrate-card-config.ts +++ b/src/panels/lovelace/cards/migrate-card-config.ts @@ -1,40 +1,56 @@ -import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; +import { migrateStateColorConfig } from "../common/entity-color-config"; +import { migrateTimeFormatConfig } from "../common/entity-time-format-config"; +import type { + ConditionalRowConfig, + LovelaceRowConfig, +} from "../entity-rows/types"; import type { EntitiesCardConfig, + EntitiesCardEntityConfig, GlanceCardConfig, GlanceConfigEntity, } from "./types"; +const migrateEntitiesRowConfig = ( + rowConf: LovelaceRowConfig | string +): LovelaceRowConfig | string => { + if (typeof rowConf !== "object") { + return rowConf; + } + let newConf: LovelaceRowConfig = rowConf; + newConf = migrateTimeFormatConfig(newConf as EntitiesCardEntityConfig); + newConf = migrateStateColorConfig(newConf as EntitiesCardEntityConfig); + if (newConf.type === "conditional") { + const row = (newConf as ConditionalRowConfig).row; + if (row && typeof row === "object") { + let newRow = migrateTimeFormatConfig(row as EntitiesCardEntityConfig); + newRow = migrateStateColorConfig(newRow); + if (newRow !== row) { + newConf = { ...newConf, row: newRow } as ConditionalRowConfig; + } + } + } + return newConf; +}; + export const migrateEntitiesCardConfig = ( config: EntitiesCardConfig ): EntitiesCardConfig => { let changed = false; const newEntities = config.entities?.map((e) => { - if (typeof e !== "object") { - return e; - } - // Custom rows own their config schema and may use `format` with a - // different meaning (e.g. custom:multiple-entity-row), so leave it - // untouched. - if (e.type?.startsWith("custom:")) { - return e; + const newConf = migrateEntitiesRowConfig(e); + if (newConf !== e) { + changed = true; } - if (!("format" in e)) { - return e; - } - changed = true; - const { format, ...rest } = e; - return { - ...rest, - time_format: (rest as EntityConfig).time_format ?? format, - }; + return newConf; }); + const newConfig = migrateStateColorConfig(config); if (!changed) { - return config; + return newConfig; } return { - ...config, - entities: newEntities as (LovelaceRowConfig | string)[], + ...newConfig, + entities: newEntities, }; }; @@ -46,21 +62,20 @@ export const migrateGlanceCardConfig = ( if (typeof e !== "object") { return e; } - if (!("format" in e)) { - return e; + let newConf = e; + newConf = migrateTimeFormatConfig(newConf); + newConf = migrateStateColorConfig(newConf); + if (newConf !== e) { + changed = true; } - changed = true; - const { format, ...rest } = e; - return { - ...rest, - time_format: rest.time_format ?? format, - }; + return newConf; }); + const newConfig = migrateStateColorConfig(config); if (!changed) { - return config; + return newConfig; } return { - ...config, + ...newConfig, entities: newEntities as (GlanceConfigEntity | string)[], }; }; diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 6850ff49f703..919d3eac06ff 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -112,7 +112,9 @@ export interface EntitiesCardEntityConfig extends EntityConfig { tap_action?: ActionConfig; hold_action?: ActionConfig; double_tap_action?: ActionConfig; + /** @deprecated use `color` instead */ state_color?: boolean; + color?: string; show_name?: boolean; show_icon?: boolean; } @@ -126,7 +128,9 @@ export interface EntitiesCardConfig extends LovelaceCardConfig { icon?: string; header?: LovelaceHeaderFooterConfig; footer?: LovelaceHeaderFooterConfig; + /** @deprecated use `color` instead */ state_color?: boolean; + color?: string; } export type AreaCardDisplayType = "compact" | "icon" | "picture" | "camera"; @@ -330,7 +334,9 @@ export interface GlanceConfigEntity extends ConfigEntity { show_last_changed?: boolean; image?: string; show_state?: boolean; + /** @deprecated use `color` instead */ state_color?: boolean; + color?: string; time_format?: TimestampRenderingFormat; } @@ -342,7 +348,9 @@ export interface GlanceCardConfig extends LovelaceCardConfig { theme?: string; entities: (string | GlanceConfigEntity)[]; columns?: number; + /** @deprecated use `color` instead */ state_color?: boolean; + color?: string; } export interface HumidifierCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/common/entity-color-config.ts b/src/panels/lovelace/common/entity-color-config.ts new file mode 100644 index 000000000000..cd2f785dfb6d --- /dev/null +++ b/src/panels/lovelace/common/entity-color-config.ts @@ -0,0 +1,34 @@ +import { isCustomType } from "../../../data/lovelace_custom_cards"; + +interface LegacyStateColorConfig { + type?: string; + color?: string; + state_color?: boolean; +} + +export const migrateStateColorConfig = ( + config: T +): T => { + // Custom elements own their config schema, leave them untouched + if (config.type !== undefined && isCustomType(config.type)) { + return config; + } + if (config.state_color === undefined) { + return config; + } + const { state_color, ...rest } = config; + return { + color: state_color ? "state" : "none", + ...rest, + } as T; +}; + +export const applyDefaultColor = ( + config: T, + color: string | undefined +): T => + color === undefined || + config.color !== undefined || + (config.type !== undefined && isCustomType(config.type)) + ? config + : { ...config, color }; diff --git a/src/panels/lovelace/common/entity-time-format-config.ts b/src/panels/lovelace/common/entity-time-format-config.ts new file mode 100644 index 000000000000..46587615e518 --- /dev/null +++ b/src/panels/lovelace/common/entity-time-format-config.ts @@ -0,0 +1,27 @@ +import { isCustomType } from "../../../data/lovelace_custom_cards"; +import type { TimestampRenderingFormat } from "../components/types"; + +interface LegacyTimeFormatConfig { + type?: string; + time_format?: TimestampRenderingFormat; + /** @deprecated use `time_format` instead */ + format?: TimestampRenderingFormat; +} + +export const migrateTimeFormatConfig = ( + config: T +): T => { + // Custom elements own their config schema and may use the same option with + // a different meaning (e.g. custom:multiple-entity-row), leave them untouched + if (config.type !== undefined && isCustomType(config.type)) { + return config; + } + if (config.format === undefined) { + return config; + } + const { format, ...rest } = config; + return { + time_format: format, + ...rest, + } as T; +}; diff --git a/src/panels/lovelace/components/hui-generic-entity-row.ts b/src/panels/lovelace/components/hui-generic-entity-row.ts index 0b3780736036..a038f956b8e8 100644 --- a/src/panels/lovelace/components/hui-generic-entity-row.ts +++ b/src/panels/lovelace/components/hui-generic-entity-row.ts @@ -86,6 +86,7 @@ export class HuiGenericEntityRow extends LitElement { .overrideIcon=${this.config.icon} .overrideImage=${this.config.image} .stateColor=${this.config.state_color} + .color=${this.config.color} > ${ !this.hideName diff --git a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts index c888ebdb2d5d..cb09148ac9a0 100644 --- a/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-entities-card-editor.ts @@ -22,11 +22,9 @@ import type { HASSDomEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event"; import { customType } from "../../../../common/structs/is-custom-type"; import "../../../../components/ha-card"; -import "../../../../components/ha-formfield"; +import "../../../../components/ha-form/ha-form"; +import type { SchemaUnion } from "../../../../components/ha-form/types"; import "../../../../components/ha-icon"; -import "../../../../components/ha-switch"; -import "../../../../components/ha-theme-picker"; -import "../../../../components/input/ha-input"; import { isCustomType } from "../../../../data/lovelace_custom_cards"; import type { HomeAssistant } from "../../../../types"; import { @@ -90,6 +88,7 @@ const conditionalEntitiesRowConfigStruct = object({ type: literal("conditional"), row: any(), conditions: array(any()), + color: optional(string()), }); const dividerEntitiesRowConfigStruct = object({ @@ -181,6 +180,22 @@ const entitiesRowConfigStruct = dynamic((value) => { return entitiesConfigStruct; }); +const SCHEMA = [ + { name: "title", selector: { text: {} } }, + { name: "theme", selector: { theme: {} } }, + { + name: "color", + selector: { + ui_color: { + default_color: "state", + include_state: true, + include_none: true, + }, + }, + }, + { name: "show_header_toggle", selector: { boolean: {} } }, +] as const; + const cardConfigStruct = assign( baseLovelaceCardConfig, object({ @@ -189,7 +204,7 @@ const cardConfigStruct = assign( theme: optional(string()), icon: optional(string()), show_header_toggle: optional(boolean()), - state_color: optional(boolean()), + color: optional(string()), entities: array(entitiesRowConfigStruct), header: optional(headerFooterConfigStructs), footer: optional(headerFooterConfigStructs), @@ -226,14 +241,6 @@ export class HuiEntitiesCardEditor ); }); - get _title(): string { - return this._config!.title || ""; - } - - get _theme(): string { - return this._config!.theme || ""; - } - protected render() { if (!this.hass || !this._config) { return nothing; @@ -251,52 +258,20 @@ export class HuiEntitiesCardEditor `; } + const data = { + ...this._config, + show_header_toggle: this._showHeaderToggle(this._config), + }; + return html`
- - -
- - - - - - -
+ ) => { + switch (schema.name) { + case "title": + case "theme": + return `${this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + )} (${this.hass!.localize( + "ui.panel.lovelace.editor.card.config.optional" + )})`; + case "show_header_toggle": + return this.hass!.localize( + "ui.panel.lovelace.editor.card.entities.show_header_toggle" + ); + default: + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + } + }; + private _valueChanged(ev: CustomEvent): void { ev.stopPropagation(); if (!this._config || !this.hass) { @@ -330,17 +342,7 @@ export class HuiEntitiesCardEditor const target = ev.target! as EditorTarget; const configValue = target.configValue || this._subElementEditorConfig?.type; - const value = - target.checked !== undefined - ? target.checked - : target.value || ev.detail.config || ev.detail.value; - - if ( - (configValue === "title" && target.value === this._title) || - (configValue === "theme" && target.value === this._theme) - ) { - return; - } + const value = ev.detail.config ?? ev.detail.value; if (configValue === "row" || (ev.detail && ev.detail.entities)) { const newConfigEntities = @@ -359,7 +361,7 @@ export class HuiEntitiesCardEditor this._config = { ...this._config!, entities: newConfigEntities }; this._configEntities = processEditorEntities(this._config!.entities); } else if (configValue) { - if (value === "") { + if (value === "" || value === undefined) { this._config = { ...this._config }; delete this._config[configValue!]; } else { @@ -435,10 +437,6 @@ export class HuiEntitiesCardEditor hui-header-footer-editor { padding-top: var(--ha-space-1); } - - ha-input { - --ha-input-padding-bottom: var(--ha-space-4); - } `, ]; } diff --git a/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts b/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts index 8bc1bd43d743..25677aacb094 100644 --- a/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-generic-entity-row-editor.ts @@ -59,13 +59,28 @@ export class HuiGenericEntityRowEditor context: { entity: "entity" }, }, { - name: "icon", - selector: { - icon: {}, - }, - context: { - icon_entity: "entity", - }, + name: "", + type: "grid", + schema: [ + { + name: "icon", + selector: { + icon: {}, + }, + context: { + icon_entity: "entity", + }, + }, + { + name: "color", + selector: { + ui_color: { + include_state: true, + include_none: true, + }, + }, + }, + ], }, { name: "secondary_info", diff --git a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts index 1952089dab85..c9006db633ff 100644 --- a/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-glance-card-editor.ts @@ -44,7 +44,7 @@ const cardConfigStruct = assign( show_name: optional(boolean()), show_state: optional(boolean()), show_icon: optional(boolean()), - state_color: optional(boolean()), + color: optional(string()), entities: array(entitiesConfigStruct), }) ); @@ -69,7 +69,16 @@ const SCHEMA = [ { name: "show_state", selector: { boolean: {} } }, ], }, - { name: "state_color", selector: { boolean: {} } }, + { + name: "color", + selector: { + ui_color: { + default_color: "state", + include_state: true, + include_none: true, + }, + }, + }, ] as const; @customElement("hui-glance-card-editor") @@ -115,6 +124,15 @@ export class HuiGlanceCardEditor icon_entity: "entity", }, }, + { + name: "color", + selector: { + ui_color: { + include_state: true, + include_none: true, + }, + }, + }, { name: "show_last_changed", selector: { boolean: {} } }, { name: "show_state", selector: { boolean: {} }, default: true }, ], diff --git a/src/panels/lovelace/editor/structs/entities-struct.ts b/src/panels/lovelace/editor/structs/entities-struct.ts index f6ec8277ff72..4db3552c837e 100644 --- a/src/panels/lovelace/editor/structs/entities-struct.ts +++ b/src/panels/lovelace/editor/structs/entities-struct.ts @@ -14,7 +14,7 @@ export const entitiesConfigStruct = union([ image: optional(string()), secondary_info: optional(string()), time_format: optional(timeFormatConfigStruct), - state_color: optional(boolean()), + color: optional(string()), tap_action: optional(actionConfigStruct), hold_action: optional(actionConfigStruct), double_tap_action: optional(actionConfigStruct), diff --git a/src/panels/lovelace/special-rows/hui-conditional-row.ts b/src/panels/lovelace/special-rows/hui-conditional-row.ts index 879e4d27da1f..752ac66b3da3 100644 --- a/src/panels/lovelace/special-rows/hui-conditional-row.ts +++ b/src/panels/lovelace/special-rows/hui-conditional-row.ts @@ -1,12 +1,9 @@ import { customElement } from "lit/decorators"; -import type { EntityCardConfig } from "../cards/types"; +import type { EntitiesCardEntityConfig } from "../cards/types"; +import { applyDefaultColor } from "../common/entity-color-config"; import { HuiConditionalBase } from "../components/hui-conditional-base"; import { createRowElement } from "../create-element/create-row-element"; -import type { - ConditionalRowConfig, - EntityConfig, - LovelaceRow, -} from "../entity-rows/types"; +import type { ConditionalRowConfig, LovelaceRow } from "../entity-rows/types"; import { fireEvent } from "../../../common/dom/fire_event"; declare global { @@ -23,13 +20,10 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow { throw new Error("No row configured"); } + const { color } = config as EntitiesCardEntityConfig; + this._element = createRowElement( - (config as EntityCardConfig).state_color - ? ({ - state_color: true, - ...(config.row as EntityConfig), - } as EntityConfig) - : config.row + applyDefaultColor(config.row as EntitiesCardEntityConfig, color) ) as LovelaceRow; } diff --git a/test/panels/lovelace/cards/migrate-card-config.test.ts b/test/panels/lovelace/cards/migrate-card-config.test.ts index 37767e9c72a2..a789341f5901 100644 --- a/test/panels/lovelace/cards/migrate-card-config.test.ts +++ b/test/panels/lovelace/cards/migrate-card-config.test.ts @@ -83,6 +83,75 @@ describe("migrateEntitiesCardConfig", () => { time_format: "datetime", }); }); + + it("migrates card and entity level state_color to color", () => { + expect( + migrateEntitiesCardConfig({ + type: "entities", + state_color: true, + entities: [ + "light.bed_light", + { entity: "switch.ac", state_color: false }, + { + entity: "sensor.humidity", + type: "simple-entity", + state_color: true, + }, + ], + } as unknown as EntitiesCardConfig) + ).toEqual({ + type: "entities", + color: "state", + entities: [ + "light.bed_light", + { entity: "switch.ac", color: "none" }, + { entity: "sensor.humidity", type: "simple-entity", color: "state" }, + ], + }); + }); + + it("keeps state_color on custom rows untouched", () => { + const customRow = { + entity: "sensor.power", + type: "custom:multiple-entity-row", + state_color: true, + }; + const config = { + type: "entities", + entities: [customRow], + } as unknown as EntitiesCardConfig; + + const result = migrateEntitiesCardConfig(config); + + expect(result).toBe(config); + expect(result.entities[0]).toEqual(customRow); + }); + + it("migrates conditional rows and their inner rows", () => { + expect( + migrateEntitiesCardConfig({ + type: "entities", + entities: [ + { + type: "conditional", + state_color: false, + conditions: [], + row: { entity: "light.bed_light", state_color: true }, + }, + ], + } as unknown as EntitiesCardConfig) + ).toEqual({ + type: "entities", + entities: [ + { + type: "conditional", + color: "none", + conditions: [], + row: { entity: "light.bed_light", color: "state" }, + }, + ], + }); + }); }); describe("migrateGlanceCardConfig", () => { @@ -124,4 +193,21 @@ describe("migrateGlanceCardConfig", () => { time_format: "datetime", }); }); + + it("migrates card and entity level state_color to color", () => { + expect( + migrateGlanceCardConfig({ + type: "glance", + state_color: false, + entities: [ + "light.bed_light", + { entity: "switch.ac", state_color: true }, + ], + } as unknown as GlanceCardConfig) + ).toEqual({ + type: "glance", + color: "none", + entities: ["light.bed_light", { entity: "switch.ac", color: "state" }], + }); + }); }); diff --git a/test/panels/lovelace/common/entity-color-config.test.ts b/test/panels/lovelace/common/entity-color-config.test.ts new file mode 100644 index 000000000000..a3848e1b6c19 --- /dev/null +++ b/test/panels/lovelace/common/entity-color-config.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest"; + +import { + applyDefaultColor, + migrateStateColorConfig, +} from "../../../../src/panels/lovelace/common/entity-color-config"; + +describe("migrateStateColorConfig", () => { + it("converts state_color true to color state", () => { + expect(migrateStateColorConfig({ state_color: true })).toEqual({ + color: "state", + }); + }); + + it("converts state_color false to color none", () => { + expect(migrateStateColorConfig({ state_color: false })).toEqual({ + color: "none", + }); + }); + + it("keeps an explicit color over state_color", () => { + expect( + migrateStateColorConfig({ state_color: true, color: "red" }) + ).toEqual({ color: "red" }); + }); + + it("keeps custom elements untouched", () => { + const config = { type: "custom:foo", state_color: true }; + expect(migrateStateColorConfig(config)).toBe(config); + }); + + it("returns the same object when there is nothing to migrate", () => { + const config = { color: "red" }; + expect(migrateStateColorConfig(config)).toBe(config); + }); +}); + +describe("applyDefaultColor", () => { + interface TestEntityConfig { + entity: string; + type?: string; + color?: string; + } + + it("applies the default color when none is set", () => { + const config: TestEntityConfig = { entity: "light.a" }; + expect(applyDefaultColor(config, "state")).toEqual({ + entity: "light.a", + color: "state", + }); + }); + + it("keeps an explicit color", () => { + const config: TestEntityConfig = { entity: "light.a", color: "red" }; + expect(applyDefaultColor(config, "state")).toBe(config); + }); + + it("returns the same object without a default color", () => { + const config: TestEntityConfig = { entity: "light.a" }; + expect(applyDefaultColor(config, undefined)).toBe(config); + }); + + it("keeps custom elements untouched", () => { + const config: TestEntityConfig = { entity: "light.a", type: "custom:foo" }; + expect(applyDefaultColor(config, "state")).toBe(config); + }); +}); From 967211faff7dad8ea901d260f209199331809e3f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 16 Jul 2026 16:15:10 +0200 Subject: [PATCH 2/2] Improve tests --- .../cards/migrate-card-config.test.ts | 12 +++++++ .../common/entity-color-config.test.ts | 35 +------------------ 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/test/panels/lovelace/cards/migrate-card-config.test.ts b/test/panels/lovelace/cards/migrate-card-config.test.ts index a789341f5901..544611df4b9b 100644 --- a/test/panels/lovelace/cards/migrate-card-config.test.ts +++ b/test/panels/lovelace/cards/migrate-card-config.test.ts @@ -110,6 +110,18 @@ describe("migrateEntitiesCardConfig", () => { }); }); + it("keeps an explicit color over the legacy state_color", () => { + expect( + migrateEntitiesCardConfig({ + type: "entities", + entities: [{ entity: "switch.ac", state_color: true, color: "red" }], + } as unknown as EntitiesCardConfig) + ).toEqual({ + type: "entities", + entities: [{ entity: "switch.ac", color: "red" }], + }); + }); + it("keeps state_color on custom rows untouched", () => { const customRow = { entity: "sensor.power", diff --git a/test/panels/lovelace/common/entity-color-config.test.ts b/test/panels/lovelace/common/entity-color-config.test.ts index a3848e1b6c19..fad6ab14294b 100644 --- a/test/panels/lovelace/common/entity-color-config.test.ts +++ b/test/panels/lovelace/common/entity-color-config.test.ts @@ -1,39 +1,6 @@ import { describe, expect, it } from "vitest"; -import { - applyDefaultColor, - migrateStateColorConfig, -} from "../../../../src/panels/lovelace/common/entity-color-config"; - -describe("migrateStateColorConfig", () => { - it("converts state_color true to color state", () => { - expect(migrateStateColorConfig({ state_color: true })).toEqual({ - color: "state", - }); - }); - - it("converts state_color false to color none", () => { - expect(migrateStateColorConfig({ state_color: false })).toEqual({ - color: "none", - }); - }); - - it("keeps an explicit color over state_color", () => { - expect( - migrateStateColorConfig({ state_color: true, color: "red" }) - ).toEqual({ color: "red" }); - }); - - it("keeps custom elements untouched", () => { - const config = { type: "custom:foo", state_color: true }; - expect(migrateStateColorConfig(config)).toBe(config); - }); - - it("returns the same object when there is nothing to migrate", () => { - const config = { color: "red" }; - expect(migrateStateColorConfig(config)).toBe(config); - }); -}); +import { applyDefaultColor } from "../../../../src/panels/lovelace/common/entity-color-config"; describe("applyDefaultColor", () => { interface TestEntityConfig {