From 9d3aafe219aa50d0a2c2b0bbc8308c6a79cc95fa Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Wed, 11 Mar 2026 05:59:01 -0700 Subject: [PATCH] Light favorite color card feature (#29995) * Light favorite color card feature * rewrite resizeObserver * code review * Apply suggestions from code review Co-authored-by: Petar Petrov * Apply suggestion Co-authored-by: Petar Petrov * Apply suggestion from @MindFreeze --------- Co-authored-by: Petar Petrov --- .../lights/ha-favorite-color-button.ts | 19 ++ .../hui-light-color-favorites-card-feature.ts | 240 ++++++++++++++++++ src/panels/lovelace/card-features/types.ts | 5 + .../create-card-feature-element.ts | 2 + .../hui-card-features-editor.ts | 3 + src/translations/en.json | 3 + 6 files changed, 272 insertions(+) create mode 100644 src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts diff --git a/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts b/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts index a9d2ab691e..b5b94947d9 100644 --- a/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts +++ b/src/dialogs/more-info/components/lights/ha-favorite-color-button.ts @@ -29,6 +29,8 @@ class MoreInfoViewLightColorPicker extends LitElement { @property({ attribute: false }) color!: LightColor; + @property({ type: Boolean, reflect: true }) wide = false; + @query("ha-outlined-icon-button", true) private _button?: HaOutlinedIconButton; @@ -108,6 +110,23 @@ class MoreInfoViewLightColorPicker extends LitElement { --md-ripple-pressed-opacity: 0; border-radius: var(--ha-border-radius-pill); } + :host([wide]) ha-outlined-icon-button { + width: 100%; + border-radius: var(--ha-favorite-color-button-border-radius); + --_container-shape: var(--ha-favorite-color-button-border-radius); + --_container-shape-start-start: var( + --ha-favorite-color-button-border-radius + ); + --_container-shape-start-end: var( + --ha-favorite-color-button-border-radius + ); + --_container-shape-end-start: var( + --ha-favorite-color-button-border-radius + ); + --_container-shape-end-end: var( + --ha-favorite-color-button-border-radius + ); + } :host([disabled]) { pointer-events: none; } diff --git a/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts b/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts new file mode 100644 index 0000000000..d7b8d7872a --- /dev/null +++ b/src/panels/lovelace/card-features/hui-light-color-favorites-card-feature.ts @@ -0,0 +1,240 @@ +import type { PropertyValues } from "lit"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, state, query } from "lit/decorators"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { UNAVAILABLE } from "../../../data/entity/entity"; +import { + computeDefaultFavoriteColors, + type LightEntity, + type LightColor, + lightSupportsFavoriteColors, +} from "../../../data/light"; +import type { HomeAssistant } from "../../../types"; +import type { LovelaceCardFeature } from "../types"; +import { cardFeatureStyles } from "./common/card-feature-styles"; +import type { + LightColorFavoritesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; +import { + type EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../../data/entity/entity_registry"; +import "../../../dialogs/more-info/components/lights/ha-favorite-color-button"; +import { actionHandler } from "../common/directives/action-handler-directive"; +import { debounce } from "../../../common/util/debounce"; + +export const supportsLightColorFavoritesCardFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; + const domain = computeDomain(stateObj.entity_id); + return domain === "light" && lightSupportsFavoriteColors(stateObj); +}; + +@customElement("hui-light-color-favorites-card-feature") +class HuiLightColorFavoritesCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + + @state() private _config?: LightColorFavoritesCardFeatureConfig; + + @state() private _entry?: EntityRegistryEntry | null; + + @state() private _favoriteColors: LightColor[] = []; + + @state() private _maxVisible = 0; + + @query(".container") private _container!: HTMLDivElement; + + private _resizeObserver?: ResizeObserver; + + private _unsubEntityRegistry?: UnsubscribeFunc; + + public connectedCallback() { + super.connectedCallback(); + this._subscribeEntityEntry(); + this.updateComplete.then(() => this._attachObserver()); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._unsubscribeEntityRegistry(); + this._resizeObserver?.disconnect(); + } + + private _unsubscribeEntityRegistry() { + if (this._unsubEntityRegistry) { + this._unsubEntityRegistry(); + this._unsubEntityRegistry = undefined; + } + } + + private _subscribeEntityEntry() { + if (this.hass && this.context?.entity_id) { + const id = this.context.entity_id; + try { + this._unsubEntityRegistry = subscribeEntityRegistry( + this.hass!.connection, + (entries) => { + const entry = entries.find((e) => e.entity_id === id); + if (entry) { + this._entry = entry; + } + } + ); + } catch (_e) { + this._entry = null; + } + } + } + + private _measure() { + const w = this._container.clientWidth; + const pillMin = 32 + 8; + this._maxVisible = Math.floor(w / pillMin); + } + + private _attachObserver(): void { + if (!this._resizeObserver) { + this._resizeObserver = new ResizeObserver( + debounce(() => this._measure(), 250, false) + ); + } + this._resizeObserver.observe(this._container); + } + + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id] as LightEntity | undefined; + } + + protected updated(changedProps: PropertyValues): void { + if (changedProps.has("context")) { + this._unsubscribeEntityRegistry(); + this._subscribeEntityEntry(); + } + + if (changedProps.has("_entry") || changedProps.has("_maxVisible")) { + if (this._entry) { + if (this._entry.options?.light?.favorite_colors) { + this._favoriteColors = + this._entry.options.light.favorite_colors.slice( + 0, + this._maxVisible + ); + } else if (this._stateObj) { + this._favoriteColors = computeDefaultFavoriteColors( + this._stateObj + ).slice(0, this._maxVisible); + } + } + } + } + + static getStubConfig(): LightColorFavoritesCardFeatureConfig { + return { + type: "light-color-favorites", + }; + } + + public setConfig(config: LightColorFavoritesCardFeatureConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + this._config = config; + } + + protected render() { + if ( + !this._config || + !this.hass || + !this.context || + !this._stateObj || + !supportsLightColorFavoritesCardFeature(this.hass, this.context) + ) { + return nothing; + } + + return html` +
+ ${this._favoriteColors.map( + (color, index) => html` +
+ + +
+ ` + )} +
+ `; + } + + private _handleColorAction(ev: CustomEvent) { + ev.stopPropagation(); + const index = (ev.target! as any).index!; + + const favorite = this._favoriteColors[index]; + this.hass!.callService("light", "turn_on", { + entity_id: this._stateObj!.entity_id, + ...favorite, + }); + } + + static get styles() { + return [ + cardFeatureStyles, + css` + .container { + position: relative; + display: flex; + user-select: none; + flex-wrap: nowrap; + align-items: center; + gap: 8px; + } + + .color { + position: relative; + display: block; + flex: 1 1 32px; + min-width: 32px; + height: 40px; + } + ha-favorite-color-button { + --ha-favorite-color-button-border-radius: var(--ha-border-radius-md); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-light-color-favorites-card-feature": HuiLightColorFavoritesCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index 4404382665..185cecbbc3 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -44,6 +44,10 @@ export interface LightColorTempCardFeatureConfig { type: "light-color-temp"; } +export interface LightColorFavoritesCardFeatureConfig { + type: "light-color-favorites"; +} + export interface LockCommandsCardFeatureConfig { type: "lock-commands"; } @@ -271,6 +275,7 @@ export type LovelaceCardFeatureConfig = | LawnMowerCommandsCardFeatureConfig | LightBrightnessCardFeatureConfig | LightColorTempCardFeatureConfig + | LightColorFavoritesCardFeatureConfig | LockCommandsCardFeatureConfig | LockOpenDoorCardFeatureConfig | MediaPlayerPlaybackCardFeatureConfig diff --git a/src/panels/lovelace/create-element/create-card-feature-element.ts b/src/panels/lovelace/create-element/create-card-feature-element.ts index 2eb85b3fa6..894e56e33e 100644 --- a/src/panels/lovelace/create-element/create-card-feature-element.ts +++ b/src/panels/lovelace/create-element/create-card-feature-element.ts @@ -22,6 +22,7 @@ import "../card-features/hui-humidifier-toggle-card-feature"; import "../card-features/hui-lawn-mower-commands-card-feature"; import "../card-features/hui-light-brightness-card-feature"; import "../card-features/hui-light-color-temp-card-feature"; +import "../card-features/hui-light-color-favorites-card-feature"; import "../card-features/hui-lock-commands-card-feature"; import "../card-features/hui-lock-open-door-card-feature"; import "../card-features/hui-media-player-playback-card-feature"; @@ -74,6 +75,7 @@ const TYPES = new Set([ "lawn-mower-commands", "light-brightness", "light-color-temp", + "light-color-favorites", "lock-commands", "lock-open-door", "media-player-playback", diff --git a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts index 4748607900..afcc7a72b0 100644 --- a/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts @@ -71,6 +71,7 @@ import type { LovelaceCardFeatureContext, } from "../../card-features/types"; import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element"; +import { supportsLightColorFavoritesCardFeature } from "../../card-features/hui-light-color-favorites-card-feature"; export type FeatureType = LovelaceCardFeatureConfig["type"]; @@ -106,6 +107,7 @@ const UI_FEATURE_TYPES = [ "lawn-mower-commands", "light-brightness", "light-color-temp", + "light-color-favorites", "lock-commands", "lock-open-door", "media-player-playback", @@ -182,6 +184,7 @@ const SUPPORTS_FEATURE_TYPES: Record< "lawn-mower-commands": supportsLawnMowerCommandCardFeature, "light-brightness": supportsLightBrightnessCardFeature, "light-color-temp": supportsLightColorTempCardFeature, + "light-color-favorites": supportsLightColorFavoritesCardFeature, "lock-commands": supportsLockCommandsCardFeature, "lock-open-door": supportsLockOpenDoorCardFeature, "media-player-playback": supportsMediaPlayerPlaybackCardFeature, diff --git a/src/translations/en.json b/src/translations/en.json index d86ad04ef2..84b7eba213 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -9414,6 +9414,9 @@ "light-brightness": { "label": "Light brightness" }, + "light-color-favorites": { + "label": "Light color favorites" + }, "light-color-temp": { "label": "Light color temperature" },