From fcf5ed7731732920cff3831921efd5a655f8ff18 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 26 May 2025 15:59:26 +0200 Subject: [PATCH] Use context instead of stateObj for card features (#25577) Co-authored-by: Bram Kragten --- src/data/lovelace_custom_cards.ts | 7 ++ .../hui-alarm-modes-card-feature.ts | 64 +++++++--- .../card-features/hui-card-feature.ts | 25 ++-- .../card-features/hui-card-features.ts | 10 +- .../hui-climate-fan-modes-card-feature.ts | 54 ++++++--- .../hui-climate-hvac-modes-card-feature.ts | 58 ++++++--- .../hui-climate-preset-modes-card-feature.ts | 54 ++++++--- ...ate-swing-horizontal-modes-card-feature.ts | 54 ++++++--- .../hui-climate-swing-modes-card-feature.ts | 54 ++++++--- .../hui-counter-actions-card-feature.ts | 41 +++++-- .../hui-cover-open-close-card-feature.ts | 54 ++++++--- .../hui-cover-position-card-feature.ts | 47 +++++--- .../hui-cover-tilt-card-feature.ts | 50 +++++--- .../hui-cover-tilt-position-card-feature.ts | 41 +++++-- .../hui-fan-preset-modes-card-feature.ts | 53 ++++++--- .../hui-fan-speed-card-feature.ts | 59 ++++++---- .../hui-humidifier-modes-card-feature.ts | 54 ++++++--- .../hui-humidifier-toggle-card-feature.ts | 54 ++++++--- .../hui-lawn-mower-commands-card-feature.ts | 39 ++++-- .../hui-light-brightness-card-feature.ts | 41 +++++-- .../hui-light-color-temp-card-feature.ts | 49 +++++--- .../hui-lock-commands-card-feature.ts | 39 ++++-- .../hui-lock-open-door-card-feature.ts | 37 ++++-- ...media-player-volume-slider-card-feature.ts | 46 ++++++-- .../hui-numeric-input-card-feature.ts | 43 +++++-- .../hui-select-options-card-feature.ts | 59 +++++++--- .../hui-target-humidity-card-feature.ts | 57 ++++++--- .../hui-target-temperature-card-feature.ts | 111 +++++++++++------- .../card-features/hui-toggle-card-feature.ts | 52 +++++--- .../hui-update-actions-card-feature.ts | 41 +++++-- .../hui-vacuum-commands-card-feature.ts | 44 +++++-- ...ter-heater-operation-modes-card-feature.ts | 54 ++++++--- .../lovelace/cards/hui-humidifier-card.ts | 8 +- .../lovelace/cards/hui-thermostat-card.ts | 8 +- src/panels/lovelace/cards/hui-tile-card.ts | 8 +- .../hui-card-features-editor.ts | 70 ++++++++--- .../hui-humidifier-card-editor.ts | 13 +- .../hui-thermostat-card-editor.ts | 13 +- .../config-elements/hui-tile-card-editor.ts | 22 ++-- .../areas/helpers/areas-strategy-helper.ts | 19 ++- src/panels/lovelace/types.ts | 13 +- 41 files changed, 1229 insertions(+), 490 deletions(-) diff --git a/src/data/lovelace_custom_cards.ts b/src/data/lovelace_custom_cards.ts index c79cfb4584..3b3a4e5486 100644 --- a/src/data/lovelace_custom_cards.ts +++ b/src/data/lovelace_custom_cards.ts @@ -1,4 +1,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; +import type { HomeAssistant } from "../types"; +import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types"; export interface CustomCardEntry { type: string; @@ -19,7 +21,12 @@ export interface CustomBadgeEntry { export interface CustomCardFeatureEntry { type: string; name?: string; + /** @deprecated Use `isSupported` */ supported?: (stateObj: HassEntity) => boolean; + isSupported?: ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext + ) => boolean; configurable?: boolean; } diff --git a/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts index 6aa71fe590..13524cac99 100644 --- a/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-alarm-modes-card-feature.ts @@ -1,7 +1,6 @@ import { mdiShieldOff } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; +import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; @@ -26,9 +25,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { AlarmModesCardFeatureConfig } from "./types"; +import type { + AlarmModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsAlarmModesCardFeature = (stateObj: HassEntity) => { +export const supportsAlarmModesCardFeature = ( + 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 === "alarm_control_panel"; }; @@ -40,7 +49,7 @@ class HuiAlarmModeCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: AlarmControlPanelEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: AlarmModesCardFeatureConfig; @@ -66,10 +75,26 @@ class HuiAlarmModeCardFeature this._config = config; } + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id] as + | AlarmControlPanelEntity + | undefined; + } + protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentMode = this._getCurrentMode(this.stateObj); + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentMode = this._getCurrentMode(this._stateObj); + } } } @@ -79,12 +104,12 @@ class HuiAlarmModeCardFeature }); private async _valueChanged(ev: CustomEvent) { - if (!this.stateObj) return; + if (!this._stateObj) return; const mode = (ev.detail as any).value as AlarmMode; - if (mode === this.stateObj.state) return; + if (mode === this._stateObj.state) return; - const oldMode = this._getCurrentMode(this.stateObj); + const oldMode = this._getCurrentMode(this._stateObj); this._currentMode = mode; try { @@ -102,24 +127,25 @@ class HuiAlarmModeCardFeature await setProtectedAlarmControlPanelMode( this, this.hass!, - this.stateObj!, + this._stateObj!, mode ); } - protected render(): TemplateResult | null { + protected render(): TemplateResult | typeof nothing { if ( !this._config || !this.hass || - !this.stateObj || - !supportsAlarmModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsAlarmModesCardFeature(this.hass, this.context) ) { - return null; + return nothing; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); - const supportedModes = supportedAlarmModes(this.stateObj).reverse(); + const supportedModes = supportedAlarmModes(this._stateObj).reverse(); const options = filterModes( supportedModes, @@ -130,7 +156,7 @@ class HuiAlarmModeCardFeature path: ALARM_MODES[mode].path, })); - if (["triggered", "arming", "pending"].includes(this.stateObj.state)) { + if (["triggered", "arming", "pending"].includes(this._stateObj.state)) { return html` `; diff --git a/src/panels/lovelace/card-features/hui-card-feature.ts b/src/panels/lovelace/card-features/hui-card-feature.ts index 7a6d3668e7..12592b4fba 100644 --- a/src/panels/lovelace/card-features/hui-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-card-feature.ts @@ -1,17 +1,19 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import type { HomeAssistant } from "../../../types"; import type { HuiErrorCard } from "../cards/hui-error-card"; import { createCardFeatureElement } from "../create-element/create-card-feature-element"; import type { LovelaceCardFeature } from "../types"; -import type { LovelaceCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; @customElement("hui-card-feature") export class HuiCardFeature extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj!: HassEntity; + @property({ attribute: false }) public context!: LovelaceCardFeatureContext; @property({ attribute: false }) public feature?: LovelaceCardFeatureConfig; @@ -22,9 +24,7 @@ export class HuiCardFeature extends LitElement { private _getFeatureElement(feature: LovelaceCardFeatureConfig) { if (!this._element) { this._element = createCardFeatureElement(feature); - return this._element; } - return this._element; } @@ -33,12 +33,21 @@ export class HuiCardFeature extends LitElement { return nothing; } - const element = this._getFeatureElement(this.feature); + const element = this._getFeatureElement( + this.feature + ) as LovelaceCardFeature; if (this.hass) { element.hass = this.hass; - (element as LovelaceCardFeature).stateObj = this.stateObj; - (element as LovelaceCardFeature).color = this.color; + element.context = this.context; + element.color = this.color; + // Backwards compatibility from custom card features + if (this.context.entity_id) { + const stateObj = this.hass.states[this.context.entity_id]; + if (stateObj) { + element.stateObj = stateObj; + } + } } return html`${element}`; } diff --git a/src/panels/lovelace/card-features/hui-card-features.ts b/src/panels/lovelace/card-features/hui-card-features.ts index 7253bb7895..b723c3f2a0 100644 --- a/src/panels/lovelace/card-features/hui-card-features.ts +++ b/src/panels/lovelace/card-features/hui-card-features.ts @@ -1,15 +1,17 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import type { HomeAssistant } from "../../../types"; import "./hui-card-feature"; -import type { LovelaceCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; @customElement("hui-card-features") export class HuiCardFeatures extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public stateObj!: HassEntity; + @property({ attribute: false }) public context!: LovelaceCardFeatureContext; @property({ attribute: false }) public features?: LovelaceCardFeatureConfig[]; @@ -24,7 +26,7 @@ export class HuiCardFeatures extends LitElement { (feature) => html` diff --git a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts index 45425225d6..7ba6cab9e0 100644 --- a/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-fan-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiFan } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateFanModesCardFeatureConfig } from "./types"; +import type { + ClimateFanModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimateFanModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimateFanModesCardFeature = ( + 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 === "climate" && @@ -36,7 +45,7 @@ class HuiClimateFanModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateFanModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimateFanModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateFanModesCardFeatureConfig { return { type: "climate-fan-modes", @@ -68,8 +86,15 @@ class HuiClimateFanModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentFanMode = this.stateObj.attributes.fan_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentFanMode = this._stateObj.attributes.fan_mode; + } } } @@ -91,7 +116,7 @@ class HuiClimateFanModesCardFeature const fanMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldFanMode = this.stateObj!.attributes.fan_mode; + const oldFanMode = this._stateObj!.attributes.fan_mode; if (fanMode === oldFanMode) return; @@ -106,7 +131,7 @@ class HuiClimateFanModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_fan_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, fan_mode: mode, }); } @@ -115,13 +140,14 @@ class HuiClimateFanModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateFanModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateFanModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.fan_modes, @@ -129,7 +155,7 @@ class HuiClimateFanModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "fan_mode", mode ), @@ -153,7 +179,7 @@ class HuiClimateFanModesCardFeature stateObj, "fan_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -165,7 +191,7 @@ class HuiClimateFanModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")} .value=${this._currentFanMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts index 266e2820c1..16168289f7 100644 --- a/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiThermostat } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -22,9 +21,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateHvacModesCardFeatureConfig } from "./types"; +import type { + ClimateHvacModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimateHvacModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimateHvacModesCardFeature = ( + 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 === "climate"; }; @@ -36,7 +45,7 @@ class HuiClimateHvacModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateHvacModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimateHvacModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateHvacModesCardFeatureConfig { return { type: "climate-hvac-modes", @@ -67,8 +85,15 @@ class HuiClimateHvacModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentHvacMode = this.stateObj.state as HvacMode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentHvacMode = this._stateObj.state as HvacMode; + } } } @@ -90,9 +115,9 @@ class HuiClimateHvacModesCardFeature const mode = (ev.detail as any).value ?? ((ev.target as any).value as HvacMode); - if (mode === this.stateObj!.state) return; + if (mode === this._stateObj!.state) return; - const oldMode = this.stateObj!.state as HvacMode; + const oldMode = this._stateObj!.state as HvacMode; this._currentHvacMode = mode; try { @@ -104,7 +129,7 @@ class HuiClimateHvacModesCardFeature private async _setMode(mode: HvacMode) { await this.hass!.callService("climate", "set_hvac_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, hvac_mode: mode, }); } @@ -113,15 +138,16 @@ class HuiClimateHvacModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateHvacModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateHvacModesCardFeature(this.hass, this.context) ) { return null; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); - const ordererHvacModes = (this.stateObj.attributes.hvac_modes || []) + const ordererHvacModes = (this._stateObj.attributes.hvac_modes || []) .concat() .sort(compareClimateHvacModes) .reverse(); @@ -131,7 +157,7 @@ class HuiClimateHvacModesCardFeature this._config.hvac_modes ).map((mode) => ({ value: mode, - label: this.hass!.formatEntityState(this.stateObj!, mode), + label: this.hass!.formatEntityState(this._stateObj!, mode), icon: html` `; diff --git a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts index 44086b5d14..38a49b2549 100644 --- a/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-preset-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimatePresetModesCardFeatureConfig } from "./types"; +import type { + ClimatePresetModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimatePresetModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimatePresetModesCardFeature = ( + 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 === "climate" && @@ -36,7 +45,7 @@ class HuiClimatePresetModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimatePresetModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimatePresetModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimatePresetModesCardFeatureConfig { return { type: "climate-preset-modes", @@ -70,8 +88,15 @@ class HuiClimatePresetModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentPresetMode = this.stateObj.attributes.preset_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentPresetMode = this._stateObj.attributes.preset_mode; + } } } @@ -93,7 +118,7 @@ class HuiClimatePresetModesCardFeature const presetMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldPresetMode = this.stateObj!.attributes.preset_mode; + const oldPresetMode = this._stateObj!.attributes.preset_mode; if (presetMode === oldPresetMode) return; @@ -108,7 +133,7 @@ class HuiClimatePresetModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_preset_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, preset_mode: mode, }); } @@ -117,13 +142,14 @@ class HuiClimatePresetModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimatePresetModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimatePresetModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.preset_modes, @@ -131,7 +157,7 @@ class HuiClimatePresetModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "preset_mode", mode ), @@ -155,7 +181,7 @@ class HuiClimatePresetModesCardFeature stateObj, "preset_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -167,7 +193,7 @@ class HuiClimatePresetModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} .value=${this._currentPresetMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts index f6579175f8..5b7c5dd127 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiArrowOscillating } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,11 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateSwingHorizontalModesCardFeatureConfig } from "./types"; +import type { + ClimateSwingHorizontalModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; export const supportsClimateSwingHorizontalModesCardFeature = ( - stateObj: HassEntity + 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 === "climate" && @@ -38,7 +45,7 @@ class HuiClimateSwingHorizontalModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig; @@ -47,6 +54,15 @@ class HuiClimateSwingHorizontalModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateSwingHorizontalModesCardFeatureConfig { return { type: "climate-swing-horizontal-modes", @@ -72,9 +88,16 @@ class HuiClimateSwingHorizontalModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentSwingHorizontalMode = - this.stateObj.attributes.swing_horizontal_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentSwingHorizontalMode = + this._stateObj.attributes.swing_horizontal_mode; + } } } @@ -97,7 +120,7 @@ class HuiClimateSwingHorizontalModesCardFeature (ev.detail as any).value ?? ((ev.target as any).value as string); const oldSwingHorizontalMode = - this.stateObj!.attributes.swing_horizontal_mode; + this._stateObj!.attributes.swing_horizontal_mode; if (swingHorizontalMode === oldSwingHorizontalMode) return; @@ -112,7 +135,7 @@ class HuiClimateSwingHorizontalModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_swing_horizontal_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, swing_horizontal_mode: mode, }); } @@ -121,13 +144,14 @@ class HuiClimateSwingHorizontalModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateSwingHorizontalModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateSwingHorizontalModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.swing_horizontal_modes, @@ -135,7 +159,7 @@ class HuiClimateSwingHorizontalModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "swing_horizontal_mode", mode ), @@ -159,7 +183,7 @@ class HuiClimateSwingHorizontalModesCardFeature stateObj, "swing_horizontal_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -174,7 +198,7 @@ class HuiClimateSwingHorizontalModesCardFeature "swing_horizontal_mode" )} .value=${this._currentSwingHorizontalMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts index c2d68431c3..df16f90064 100644 --- a/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiArrowOscillating } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { ClimateSwingModesCardFeatureConfig } from "./types"; +import type { + ClimateSwingModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsClimateSwingModesCardFeature = (stateObj: HassEntity) => { +export const supportsClimateSwingModesCardFeature = ( + 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 === "climate" && @@ -36,7 +45,7 @@ class HuiClimateSwingModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: ClimateEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ClimateSwingModesCardFeatureConfig; @@ -45,6 +54,15 @@ class HuiClimateSwingModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | ClimateEntity + | undefined; + } + static getStubConfig(): ClimateSwingModesCardFeatureConfig { return { type: "climate-swing-modes", @@ -70,8 +88,15 @@ class HuiClimateSwingModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentSwingMode = this.stateObj.attributes.swing_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentSwingMode = this._stateObj.attributes.swing_mode; + } } } @@ -93,7 +118,7 @@ class HuiClimateSwingModesCardFeature const swingMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldSwingMode = this.stateObj!.attributes.swing_mode; + const oldSwingMode = this._stateObj!.attributes.swing_mode; if (swingMode === oldSwingMode) return; @@ -108,7 +133,7 @@ class HuiClimateSwingModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("climate", "set_swing_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, swing_mode: mode, }); } @@ -117,13 +142,14 @@ class HuiClimateSwingModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsClimateSwingModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsClimateSwingModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.swing_modes, @@ -131,7 +157,7 @@ class HuiClimateSwingModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "swing_mode", mode ), @@ -155,7 +181,7 @@ class HuiClimateSwingModesCardFeature stateObj, "swing_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -167,7 +193,7 @@ class HuiClimateSwingModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "swing_mode")} .value=${this._currentSwingMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts b/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts index d4373a6827..896b4e3aea 100644 --- a/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-counter-actions-card-feature.ts @@ -1,19 +1,30 @@ -import { mdiRestore, mdiPlus, mdiMinus } from "@mdi/js"; +import { mdiMinus, mdiPlus, mdiRestore } from "@mdi/js"; import type { HassEntity } from "home-assistant-js-websocket"; import type { TemplateResult } from "lit"; import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; +import "../../../components/ha-control-button"; +import "../../../components/ha-control-button-group"; import "../../../components/ha-control-select"; import { UNAVAILABLE } from "../../../data/entity"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import { COUNTER_ACTIONS, type CounterActionsCardFeatureConfig } from "./types"; -import "../../../components/ha-control-button-group"; -import "../../../components/ha-control-button"; +import { + COUNTER_ACTIONS, + type CounterActionsCardFeatureConfig, + type LovelaceCardFeatureContext, +} from "./types"; -export const supportsCounterActionsCardFeature = (stateObj: HassEntity) => { +export const supportsCounterActionsCardFeature = ( + 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 === "counter"; }; @@ -56,10 +67,17 @@ class HuiCounterActionsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: CounterActionsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as HassEntity | undefined; + } + public static async getConfigElement(): Promise { await import( "../editor/config-elements/hui-counter-actions-card-feature-editor" @@ -85,8 +103,9 @@ class HuiCounterActionsCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCounterActionsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCounterActionsCardFeature(this.hass, this.context) ) { return null; } @@ -96,7 +115,7 @@ class HuiCounterActionsCardFeature ${this._config?.actions ?.filter((action) => COUNTER_ACTIONS.includes(action)) .map((action) => { - const button = COUNTER_ACTIONS_BUTTON[action](this.stateObj!); + const button = COUNTER_ACTIONS_BUTTON[action](this._stateObj!); return html` @@ -120,7 +139,7 @@ class HuiCounterActionsCardFeature ev.stopPropagation(); const entry = (ev.target! as any).entry as CounterButton; this.hass!.callService("counter", entry.serviceName, { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } diff --git a/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts index 0b4ec5e5f3..e462c06522 100644 --- a/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-open-close-card-feature.ts @@ -1,5 +1,4 @@ import { mdiStop } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; @@ -9,20 +8,31 @@ import { } from "../../../common/entity/cover_icon"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { canClose, canOpen, canStop, CoverEntityFeature, + type CoverEntity, } from "../../../data/cover"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverOpenCloseCardFeatureConfig } from "./types"; +import type { + CoverOpenCloseCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsCoverOpenCloseCardFeature = (stateObj: HassEntity) => { +export const supportsCoverOpenCloseCardFeature = ( + 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 === "cover" && @@ -38,10 +48,17 @@ class HuiCoverOpenCloseCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: CoverOpenCloseCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverOpenCloseCardFeatureConfig { return { type: "cover-open-close", @@ -58,21 +75,21 @@ class HuiCoverOpenCloseCardFeature private _onOpenTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "open_cover", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onCloseTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "close_cover", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onStopTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "stop_cover", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -80,47 +97,48 @@ class HuiCoverOpenCloseCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverOpenCloseCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCoverOpenCloseCardFeature(this.hass, this.context) ) { return nothing; } return html` - ${supportsFeature(this.stateObj, CoverEntityFeature.OPEN) + ${supportsFeature(this._stateObj, CoverEntityFeature.OPEN) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.STOP) + ${supportsFeature(this._stateObj, CoverEntityFeature.STOP) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) + ${supportsFeature(this._stateObj, CoverEntityFeature.CLOSE) ? html` ` diff --git a/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts index 3fd595e447..81731ef8f0 100644 --- a/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-position-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; @@ -8,16 +7,26 @@ import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; import { stateColorCss } from "../../../common/entity/state_color"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import { CoverEntityFeature } from "../../../data/cover"; +import "../../../components/ha-control-slider"; +import { CoverEntityFeature, type CoverEntity } from "../../../data/cover"; import { UNAVAILABLE } from "../../../data/entity"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverPositionCardFeatureConfig } from "./types"; -import "../../../components/ha-control-slider"; +import type { + CoverPositionCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsCoverPositionCardFeature = (stateObj: HassEntity) => { +export const supportsCoverPositionCardFeature = ( + 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 === "cover" && @@ -32,12 +41,19 @@ class HuiCoverPositionCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @property({ attribute: false }) public color?: string; @state() private _config?: CoverPositionCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverPositionCardFeatureConfig { return { type: "cover-position", @@ -55,23 +71,24 @@ class HuiCoverPositionCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverPositionCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCoverPositionCardFeature(this.hass, this.context) ) { return nothing; } - const percentage = stateActive(this.stateObj) - ? (this.stateObj.attributes.current_position ?? 0) + const percentage = stateActive(this._stateObj) + ? (this._stateObj.attributes.current_position ?? 0) : 0; const value = Math.max(Math.round(percentage), 0); - const openColor = stateColorCss(this.stateObj, "open"); + const openColor = stateColorCss(this._stateObj, "open"); const color = this.color ? computeCssColor(this.color) - : stateColorCss(this.stateObj); + : stateColorCss(this._stateObj); const style = { "--feature-color": color, @@ -91,11 +108,11 @@ class HuiCoverPositionCardFeature @value-changed=${this._valueChanged} .ariaLabel=${computeAttributeNameDisplay( this.hass.localize, - this.stateObj, + this._stateObj, this.hass.entities, "current_position" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_position} .locale=${this.hass.locale} > @@ -107,7 +124,7 @@ class HuiCoverPositionCardFeature if (isNaN(value)) return; this.hass!.callService("cover", "set_cover_position", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, position: value, }); } diff --git a/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts index 9c637bb192..0d18965beb 100644 --- a/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-tilt-card-feature.ts @@ -1,24 +1,34 @@ import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { CoverEntityFeature, canCloseTilt, canOpenTilt, canStopTilt, + type CoverEntity, } from "../../../data/cover"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverTiltCardFeatureConfig } from "./types"; +import type { + CoverTiltCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsCoverTiltCardFeature = (stateObj: HassEntity) => { +export const supportsCoverTiltCardFeature = ( + 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 === "cover" && @@ -34,10 +44,17 @@ class HuiCoverTiltCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: CoverTiltCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverTiltCardFeatureConfig { return { type: "cover-tilt", @@ -54,21 +71,21 @@ class HuiCoverTiltCardFeature private _onOpenTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "open_cover_tilt", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onCloseTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "close_cover_tilt", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } private _onStopTap(ev): void { ev.stopPropagation(); this.hass!.callService("cover", "stop_cover_tilt", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -76,42 +93,43 @@ class HuiCoverTiltCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverTiltCardFeature + !this.context || + !this._stateObj || + !supportsCoverTiltCardFeature(this.hass, this.context) ) { return nothing; } return html` - ${supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) + ${supportsFeature(this._stateObj, CoverEntityFeature.OPEN_TILT) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT) + ${supportsFeature(this._stateObj, CoverEntityFeature.STOP_TILT) ? html` ` : nothing} - ${supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) + ${supportsFeature(this._stateObj, CoverEntityFeature.CLOSE_TILT) ? html` diff --git a/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts b/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts index 2b4ed1e2bd..15c4a5f154 100644 --- a/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-cover-tilt-position-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; @@ -15,11 +14,21 @@ import { generateTiltSliderTrackBackgroundGradient } from "../../../state-contro import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { CoverTiltPositionCardFeatureConfig } from "./types"; +import type { + CoverTiltPositionCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; const GRADIENT = generateTiltSliderTrackBackgroundGradient(); -export const supportsCoverTiltPositionCardFeature = (stateObj: HassEntity) => { +export const supportsCoverTiltPositionCardFeature = ( + 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 === "cover" && @@ -34,12 +43,19 @@ class HuiCoverTiltPositionCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: CoverEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @property({ attribute: false }) public color?: string; @state() private _config?: CoverTiltPositionCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as CoverEntity | undefined; + } + static getStubConfig(): CoverTiltPositionCardFeatureConfig { return { type: "cover-tilt-position", @@ -57,21 +73,22 @@ class HuiCoverTiltPositionCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsCoverTiltPositionCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsCoverTiltPositionCardFeature(this.hass, this.context) ) { return nothing; } - const percentage = this.stateObj.attributes.current_tilt_position ?? 0; + const percentage = this._stateObj.attributes.current_tilt_position ?? 0; const value = Math.max(Math.round(percentage), 0); - const openColor = stateColorCss(this.stateObj, "open"); + const openColor = stateColorCss(this._stateObj, "open"); const color = this.color ? computeCssColor(this.color) - : stateColorCss(this.stateObj); + : stateColorCss(this._stateObj); const style = { "--feature-color": color, @@ -90,11 +107,11 @@ class HuiCoverTiltPositionCardFeature @value-changed=${this._valueChanged} .ariaLabel=${computeAttributeNameDisplay( this.hass.localize, - this.stateObj, + this._stateObj, this.hass.entities, "current_tilt_position" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .unit=${DOMAIN_ATTRIBUTES_UNITS.cover.current_tilt_position} .locale=${this.hass.locale} > @@ -108,7 +125,7 @@ class HuiCoverTiltPositionCardFeature if (isNaN(value)) return; this.hass!.callService("cover", "set_cover_tilt_position", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, tilt_position: value, }); } diff --git a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts index 5abdbc128c..bd70400778 100644 --- a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { FanPresetModesCardFeatureConfig } from "./types"; +import type { + FanPresetModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsFanPresetModesCardFeature = (stateObj: HassEntity) => { +export const supportsFanPresetModesCardFeature = ( + 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 === "fan" && supportsFeature(stateObj, FanEntityFeature.PRESET_MODE) @@ -35,7 +44,7 @@ class HuiFanPresetModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: FanEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: FanPresetModesCardFeatureConfig; @@ -44,6 +53,13 @@ class HuiFanPresetModesCardFeature @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as FanEntity | undefined; + } + static getStubConfig(): FanPresetModesCardFeatureConfig { return { type: "fan-preset-modes", @@ -66,9 +82,15 @@ class HuiFanPresetModesCardFeature } protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentPresetMode = this.stateObj.attributes.preset_mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentPresetMode = this._stateObj.attributes.preset_mode; + } } } @@ -90,7 +112,7 @@ class HuiFanPresetModesCardFeature const presetMode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldPresetMode = this.stateObj!.attributes.preset_mode; + const oldPresetMode = this._stateObj!.attributes.preset_mode; if (presetMode === oldPresetMode) return; @@ -105,7 +127,7 @@ class HuiFanPresetModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("fan", "set_preset_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, preset_mode: mode, }); } @@ -114,13 +136,14 @@ class HuiFanPresetModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsFanPresetModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsFanPresetModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.preset_modes, @@ -128,7 +151,7 @@ class HuiFanPresetModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "preset_mode", mode ), @@ -152,7 +175,7 @@ class HuiFanPresetModesCardFeature stateObj, "preset_mode" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -164,7 +187,7 @@ class HuiFanPresetModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "preset_mode")} .value=${this._currentPresetMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts index 0be0146872..a809a124ee 100644 --- a/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-speed-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display"; @@ -9,6 +8,7 @@ import "../../../components/ha-control-select"; import type { ControlSelectOption } from "../../../components/ha-control-select"; import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity"; +import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import type { FanEntity, FanSpeed } from "../../../data/fan"; import { computeFanSpeedCount, @@ -21,11 +21,20 @@ import { } from "../../../data/fan"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; -import type { FanSpeedCardFeatureConfig } from "./types"; -import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; import { cardFeatureStyles } from "./common/card-feature-styles"; +import type { + FanSpeedCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsFanSpeedCardFeature = (stateObj: HassEntity) => { +export const supportsFanSpeedCardFeature = ( + 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 === "fan" && supportsFeature(stateObj, FanEntityFeature.SET_SPEED) @@ -36,10 +45,17 @@ export const supportsFanSpeedCardFeature = (stateObj: HassEntity) => { class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: FanEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: FanSpeedCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as FanEntity | undefined; + } + static getStubConfig(): FanSpeedCardFeatureConfig { return { type: "fan-speed", @@ -55,7 +71,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { private _localizeSpeed(speed: FanSpeed) { if (speed === "on" || speed === "off") { - return this.hass!.formatEntityState(this.stateObj!, speed); + return this.hass!.formatEntityState(this._stateObj!, speed); } return this.hass!.localize(`ui.card.fan.speed.${speed}`) || speed; } @@ -64,16 +80,17 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { if ( !this._config || !this.hass || - !this.stateObj || - !supportsFanSpeedCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsFanSpeedCardFeature(this.hass, this.context) ) { return nothing; } - const speedCount = computeFanSpeedCount(this.stateObj); + const speedCount = computeFanSpeedCount(this._stateObj); - const percentage = stateActive(this.stateObj) - ? (this.stateObj.attributes.percentage ?? 0) + const percentage = stateActive(this._stateObj) + ? (this._stateObj.attributes.percentage ?? 0) : 0; if (speedCount <= FAN_SPEED_COUNT_MAX_FOR_BUTTONS) { @@ -81,11 +98,11 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { (speed) => ({ value: speed, label: this._localizeSpeed(speed), - path: computeFanSpeedIcon(this.stateObj!, speed), + path: computeFanSpeedIcon(this._stateObj!, speed), }) ); - const speed = fanPercentageToSpeed(this.stateObj, percentage); + const speed = fanPercentageToSpeed(this._stateObj, percentage); return html` `; @@ -112,15 +129,15 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { .value=${value} min="0" max="100" - .step=${this.stateObj.attributes.percentage_step ?? 1} + .step=${this._stateObj.attributes.percentage_step ?? 1} @value-changed=${this._valueChanged} .ariaLabel=${computeAttributeNameDisplay( this.hass.localize, - this.stateObj, + this._stateObj, this.hass.entities, "percentage" )} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .unit=${DOMAIN_ATTRIBUTES_UNITS.fan.percentage} .locale=${this.hass.locale} > @@ -130,10 +147,10 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { private _speedValueChanged(ev: CustomEvent) { const speed = (ev.detail as any).value as FanSpeed; - const percentage = fanSpeedToPercentage(this.stateObj!, speed); + const percentage = fanSpeedToPercentage(this._stateObj!, speed); this.hass!.callService("fan", "set_percentage", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, percentage: percentage, }); } @@ -143,7 +160,7 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature { if (isNaN(value)) return; this.hass!.callService("fan", "set_percentage", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, percentage: value, }); } diff --git a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts index 357da0736d..82e4c4dc37 100644 --- a/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-humidifier-modes-card-feature.ts @@ -1,5 +1,4 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -19,9 +18,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { HumidifierModesCardFeatureConfig } from "./types"; +import type { + HumidifierModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsHumidifierModesCardFeature = (stateObj: HassEntity) => { +export const supportsHumidifierModesCardFeature = ( + 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 === "humidifier" && @@ -36,12 +45,21 @@ class HuiHumidifierModesCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HumidifierEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: HumidifierModesCardFeatureConfig; @state() _currentMode?: string; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | HumidifierEntity + | undefined; + } + @query("ha-control-select-menu", true) private _haSelect?: HaControlSelectMenu; @@ -68,8 +86,15 @@ class HuiHumidifierModesCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentMode = this.stateObj.attributes.mode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentMode = this._stateObj.attributes.mode; + } } } @@ -91,7 +116,7 @@ class HuiHumidifierModesCardFeature const mode = (ev.detail as any).value ?? ((ev.target as any).value as string); - const oldMode = this.stateObj!.attributes.mode; + const oldMode = this._stateObj!.attributes.mode; if (mode === oldMode) return; @@ -106,7 +131,7 @@ class HuiHumidifierModesCardFeature private async _setMode(mode: string) { await this.hass!.callService("humidifier", "set_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, mode: mode, }); } @@ -115,13 +140,14 @@ class HuiHumidifierModesCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsHumidifierModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsHumidifierModesCardFeature(this.hass, this.context) ) { return null; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = filterModes( stateObj.attributes.available_modes, @@ -129,7 +155,7 @@ class HuiHumidifierModesCardFeature ).map((mode) => ({ value: mode, label: this.hass!.formatEntityAttributeValue( - this.stateObj!, + this._stateObj!, "mode", mode ), @@ -150,7 +176,7 @@ class HuiHumidifierModesCardFeature @value-changed=${this._valueChanged} hide-label .ariaLabel=${this.hass!.formatEntityAttributeName(stateObj, "mode")} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; @@ -162,7 +188,7 @@ class HuiHumidifierModesCardFeature hide-label .label=${this.hass!.formatEntityAttributeName(stateObj, "mode")} .value=${this._currentMode} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts index 575437c04d..4cff297c96 100644 --- a/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts @@ -1,5 +1,4 @@ import { mdiPower, mdiWaterPercent } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { LitElement, html } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -16,9 +15,19 @@ import type { import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { HumidifierToggleCardFeatureConfig } from "./types"; +import type { + HumidifierToggleCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsHumidifierToggleCardFeature = (stateObj: HassEntity) => { +export const supportsHumidifierToggleCardFeature = ( + 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 === "humidifier"; }; @@ -30,12 +39,21 @@ class HuiHumidifierToggleCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HumidifierEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: HumidifierToggleCardFeatureConfig; @state() _currentState?: HumidifierState; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | HumidifierEntity + | undefined; + } + static getStubConfig(): HumidifierToggleCardFeatureConfig { return { type: "humidifier-toggle", @@ -51,17 +69,24 @@ class HuiHumidifierToggleCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentState = this.stateObj.state as HumidifierState; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentState = this._stateObj.state as HumidifierState; + } } } private async _valueChanged(ev: CustomEvent) { const newState = (ev.detail as any).value as HumidifierState; - if (newState === this.stateObj!.state) return; + if (newState === this._stateObj!.state) return; - const oldState = this.stateObj!.state as HumidifierState; + const oldState = this._stateObj!.state as HumidifierState; this._currentState = newState; try { @@ -76,7 +101,7 @@ class HuiHumidifierToggleCardFeature "humidifier", newState === "on" ? "turn_on" : "turn_off", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, } ); } @@ -85,17 +110,18 @@ class HuiHumidifierToggleCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsHumidifierToggleCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsHumidifierToggleCardFeature(this.hass, this.context) ) { return null; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); const options = ["off", "on"].map((entityState) => ({ value: entityState, - label: this.hass!.formatEntityState(this.stateObj!, entityState), + label: this.hass!.formatEntityState(this._stateObj!, entityState), path: entityState === "on" ? mdiWaterPercent : mdiPower, })); @@ -109,7 +135,7 @@ class HuiHumidifierToggleCardFeature style=${styleMap({ "--control-select-color": color, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; diff --git a/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts b/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts index 70306fd455..5b7debe756 100644 --- a/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-lawn-mower-commands-card-feature.ts @@ -5,8 +5,8 @@ import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { UNAVAILABLE } from "../../../data/entity"; import type { LawnMowerEntity } from "../../../data/lawn_mower"; import { LawnMowerEntityFeature, canDock } from "../../../data/lawn_mower"; @@ -16,6 +16,7 @@ import { cardFeatureStyles } from "./common/card-feature-styles"; import type { LawnMowerCommand, LawnMowerCommandsCardFeatureConfig, + LovelaceCardFeatureContext, } from "./types"; import { LAWN_MOWER_COMMANDS } from "./types"; @@ -74,7 +75,14 @@ export const LAWN_MOWER_COMMANDS_BUTTONS: Record< }), }; -export const supportsLawnMowerCommandCardFeature = (stateObj: HassEntity) => { +export const supportsLawnMowerCommandCardFeature = ( + 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 === "lawn_mower" && @@ -89,14 +97,26 @@ class HuiLawnMowerCommandCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LawnMowerCommandsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | LawnMowerEntity + | undefined; + } + static getStubConfig( - _, - stateObj?: HassEntity + hass: HomeAssistant, + context: LovelaceCardFeatureContext ): LawnMowerCommandsCardFeatureConfig { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; return { type: "lawn-mower-commands", commands: stateObj @@ -127,7 +147,7 @@ class HuiLawnMowerCommandCardFeature ev.stopPropagation(); const entry = (ev.target! as any).entry as LawnMowerButton; this.hass!.callService("lawn_mower", entry.serviceName, { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -135,13 +155,14 @@ class HuiLawnMowerCommandCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLawnMowerCommandCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLawnMowerCommandCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj as LawnMowerEntity; + const stateObj = this._stateObj as LawnMowerEntity; return html` diff --git a/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts b/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts index ae48e0a74f..a0c2ef042a 100644 --- a/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-brightness-card-feature.ts @@ -1,17 +1,26 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity"; -import { lightSupportsBrightness } from "../../../data/light"; +import { lightSupportsBrightness, type LightEntity } from "../../../data/light"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { LightBrightnessCardFeatureConfig } from "./types"; +import type { + LightBrightnessCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsLightBrightnessCardFeature = (stateObj: HassEntity) => { +export const supportsLightBrightnessCardFeature = ( + 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" && lightSupportsBrightness(stateObj); }; @@ -23,10 +32,17 @@ class HuiLightBrightnessCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LightBrightnessCardFeatureConfig; + 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; + } + static getStubConfig(): LightBrightnessCardFeatureConfig { return { type: "light-brightness", @@ -44,16 +60,17 @@ class HuiLightBrightnessCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLightBrightnessCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLightBrightnessCardFeature(this.hass, this.context) ) { return nothing; } const position = - this.stateObj.attributes.brightness != null + this._stateObj.attributes.brightness != null ? Math.max( - Math.round((this.stateObj.attributes.brightness * 100) / 255), + Math.round((this._stateObj.attributes.brightness * 100) / 255), 1 ) : undefined; @@ -63,8 +80,8 @@ class HuiLightBrightnessCardFeature .value=${position} min="1" max="100" - .showHandle=${stateActive(this.stateObj)} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .showHandle=${stateActive(this._stateObj)} + .disabled=${this._stateObj!.state === UNAVAILABLE} @value-changed=${this._valueChanged} .label=${this.hass.localize("ui.card.light.brightness")} unit="%" @@ -78,7 +95,7 @@ class HuiLightBrightnessCardFeature const value = ev.detail.value; this.hass!.callService("light", "turn_on", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, brightness_pct: value, }); } diff --git a/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts b/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts index 4675b293a6..93a91a9ec2 100644 --- a/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-light-color-temp-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; @@ -12,14 +11,28 @@ import { stateActive } from "../../../common/entity/state_active"; import "../../../components/ha-control-slider"; import { UNAVAILABLE } from "../../../data/entity"; import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity_attributes"; -import { LightColorMode, lightSupportsColorMode } from "../../../data/light"; +import { + LightColorMode, + lightSupportsColorMode, + type LightEntity, +} from "../../../data/light"; import { generateColorTemperatureGradient } from "../../../dialogs/more-info/components/lights/light-color-temp-picker"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { LightColorTempCardFeatureConfig } from "./types"; +import type { + LightColorTempCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsLightColorTempCardFeature = (stateObj: HassEntity) => { +export const supportsLightColorTempCardFeature = ( + 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" && @@ -34,10 +47,17 @@ class HuiLightColorTempCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LightColorTempCardFeatureConfig; + 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; + } + static getStubConfig(): LightColorTempCardFeatureConfig { return { type: "light-color-temp", @@ -55,21 +75,22 @@ class HuiLightColorTempCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLightColorTempCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLightColorTempCardFeature(this.hass, this.context) ) { return nothing; } const position = - this.stateObj.attributes.color_temp_kelvin != null - ? this.stateObj.attributes.color_temp_kelvin + this._stateObj.attributes.color_temp_kelvin != null + ? this._stateObj.attributes.color_temp_kelvin : undefined; const minKelvin = - this.stateObj.attributes.min_color_temp_kelvin ?? DEFAULT_MIN_KELVIN; + this._stateObj.attributes.min_color_temp_kelvin ?? DEFAULT_MIN_KELVIN; const maxKelvin = - this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN; + this._stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN; const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin); @@ -77,8 +98,8 @@ class HuiLightColorTempCardFeature { +export const supportsLockCommandsCardFeature = ( + 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 === "lock"; }; @@ -30,10 +39,17 @@ class HuiLockCommandsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: LockCommandsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as LockEntity | undefined; + } + static getStubConfig(): LockCommandsCardFeatureConfig { return { type: "lock-commands", @@ -50,19 +66,20 @@ class HuiLockCommandsCardFeature private _onTap(ev): void { ev.stopPropagation(); const service = ev.target.dataset.service; - if (!this.hass || !this.stateObj || !service) { + if (!this.hass || !this._stateObj || !service) { return; } forwardHaptic("light"); - callProtectedLockService(this, this.hass, this.stateObj, service); + callProtectedLockService(this, this.hass, this._stateObj, service); } protected render() { if ( !this._config || !this.hass || - !this.stateObj || - !supportsLockCommandsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLockCommandsCardFeature(this.hass, this.context) ) { return nothing; } @@ -71,7 +88,7 @@ class HuiLockCommandsCardFeature @@ -79,7 +96,7 @@ class HuiLockCommandsCardFeature diff --git a/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts index e0a7bc4eff..194f198426 100644 --- a/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-lock-open-door-card-feature.ts @@ -1,10 +1,8 @@ import { mdiCheck } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import type { CSSResultGroup } from "lit"; import { css, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; - import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; import "../../../components/ha-control-button-group"; @@ -12,13 +10,24 @@ import { callProtectedLockService, canOpen, LockEntityFeature, + type LockEntity, } from "../../../data/lock"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; -import type { LockOpenDoorCardFeatureConfig } from "./types"; import { cardFeatureStyles } from "./common/card-feature-styles"; +import type { + LockOpenDoorCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; -export const supportsLockOpenDoorCardFeature = (stateObj: HassEntity) => { +export const supportsLockOpenDoorCardFeature = ( + 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 === "lock" && supportsFeature(stateObj, LockEntityFeature.OPEN); }; @@ -35,7 +44,7 @@ class HuiLockOpenDoorCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() public _buttonState: ButtonState = "normal"; @@ -43,6 +52,13 @@ class HuiLockOpenDoorCardFeature private _buttonTimeout?: number; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as LockEntity | undefined; + } + static getStubConfig(): LockOpenDoorCardFeatureConfig { return { type: "lock-open-door", @@ -71,10 +87,10 @@ class HuiLockOpenDoorCardFeature this._setButtonState("confirm", CONFIRM_TIMEOUT_SECOND); return; } - if (!this.hass || !this.stateObj) { + if (!this.hass || !this._stateObj) { return; } - callProtectedLockService(this, this.hass, this.stateObj!, "open"); + callProtectedLockService(this, this.hass, this._stateObj!, "open"); this._setButtonState("done", DONE_TIMEOUT_SECOND); } @@ -83,8 +99,9 @@ class HuiLockOpenDoorCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsLockOpenDoorCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsLockOpenDoorCardFeature(this.hass, this.context) ) { return nothing; } @@ -100,7 +117,7 @@ class HuiLockOpenDoorCardFeature : html` diff --git a/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts b/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts index 3aeffe2b19..c330b6e1cb 100644 --- a/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-media-player-volume-slider-card-feature.ts @@ -1,20 +1,30 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateActive } from "../../../common/entity/state_active"; +import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-slider"; import { isUnavailableState } from "../../../data/entity"; +import { + MediaPlayerEntityFeature, + type MediaPlayerEntity, +} from "../../../data/media-player"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { MediaPlayerVolumeSliderCardFeatureConfig } from "./types"; -import { MediaPlayerEntityFeature } from "../../../data/media-player"; -import { supportsFeature } from "../../../common/entity/supports-feature"; +import type { + LovelaceCardFeatureContext, + MediaPlayerVolumeSliderCardFeatureConfig, +} from "./types"; export const supportsMediaPlayerVolumeSliderCardFeature = ( - stateObj: HassEntity + 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 === "media_player" && @@ -29,10 +39,19 @@ class HuiMediaPlayerVolumeSliderCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: MediaPlayerVolumeSliderCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | MediaPlayerEntity + | undefined; + } + static getStubConfig(): MediaPlayerVolumeSliderCardFeatureConfig { return { type: "media-player-volume-slider", @@ -50,15 +69,16 @@ class HuiMediaPlayerVolumeSliderCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsMediaPlayerVolumeSliderCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsMediaPlayerVolumeSliderCardFeature(this.hass, this.context) ) { return nothing; } const position = - this.stateObj.attributes.volume_level != null - ? Math.round(this.stateObj.attributes.volume_level * 100) + this._stateObj.attributes.volume_level != null + ? Math.round(this._stateObj.attributes.volume_level * 100) : undefined; return html` @@ -66,8 +86,8 @@ class HuiMediaPlayerVolumeSliderCardFeature .value=${position} min="0" max="100" - .showHandle=${stateActive(this.stateObj)} - .disabled=${!this.stateObj || isUnavailableState(this.stateObj.state)} + .showHandle=${stateActive(this._stateObj)} + .disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)} @value-changed=${this._valueChanged} unit="%" .locale=${this.hass.locale} @@ -80,7 +100,7 @@ class HuiMediaPlayerVolumeSliderCardFeature const value = ev.detail.value; this.hass!.callService("media_player", "volume_set", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, volume_level: value / 100, }); } diff --git a/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts b/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts index 561ce890cf..60c9a89cdf 100644 --- a/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-numeric-input-card-feature.ts @@ -12,9 +12,19 @@ import { isUnavailableState } from "../../../data/entity"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { NumericInputCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + NumericInputCardFeatureConfig, +} from "./types"; -export const supportsNumericInputCardFeature = (stateObj: HassEntity) => { +export const supportsNumericInputCardFeature = ( + 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 === "input_number" || domain === "number"; }; @@ -26,7 +36,7 @@ class HuiNumericInputCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: NumericInputCardFeatureConfig; @@ -39,6 +49,13 @@ class HuiNumericInputCardFeature }; } + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as HassEntity | undefined; + } + public static async getConfigElement(): Promise { await import( "../editor/config-elements/hui-numeric-input-card-feature-editor" @@ -55,13 +72,20 @@ class HuiNumericInputCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentState = this.stateObj.state; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentState = this._stateObj.state; + } } } private async _setValue(ev: CustomEvent) { - const stateObj = this.stateObj!; + const stateObj = this._stateObj!; const domain = computeDomain(stateObj.entity_id); @@ -75,13 +99,14 @@ class HuiNumericInputCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsNumericInputCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsNumericInputCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const parsedState = Number(stateObj.state); const value = !isNaN(parsedState) ? parsedState : undefined; diff --git a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts index bb13f1c206..f58f80d8b4 100644 --- a/src/panels/lovelace/card-features/hui-select-options-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-select-options-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, query, state } from "lit/decorators"; @@ -15,9 +14,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { SelectOptionsCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + SelectOptionsCardFeatureConfig, +} from "./types"; -export const supportsSelectOptionsCardFeature = (stateObj: HassEntity) => { +export const supportsSelectOptionsCardFeature = ( + 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 === "select" || domain === "input_select"; }; @@ -29,9 +38,7 @@ class HuiSelectOptionsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: - | SelectEntity - | InputSelectEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: SelectOptionsCardFeatureConfig; @@ -40,6 +47,16 @@ class HuiSelectOptionsCardFeature @query("ha-control-select-menu", true) private _haSelect!: HaControlSelectMenu; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | SelectEntity + | InputSelectEntity + | undefined; + } + static getStubConfig(): SelectOptionsCardFeatureConfig { return { type: "select-options", @@ -62,8 +79,15 @@ class HuiSelectOptionsCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentOption = this.stateObj.state; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentOption = this._stateObj.state; + } } } @@ -84,11 +108,11 @@ class HuiSelectOptionsCardFeature private async _valueChanged(ev: CustomEvent) { const option = (ev.target as any).value as string; - const oldOption = this.stateObj!.state; + const oldOption = this._stateObj!.state; if ( option === oldOption || - !this.stateObj!.attributes.options.includes(option) + !this._stateObj!.attributes.options.includes(option) ) return; @@ -102,9 +126,9 @@ class HuiSelectOptionsCardFeature } private async _setOption(option: string) { - const domain = computeDomain(this.stateObj!.entity_id); + const domain = computeDomain(this._stateObj!.entity_id); await this.hass!.callService(domain, "select_option", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, option: option, }); } @@ -113,16 +137,17 @@ class HuiSelectOptionsCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsSelectOptionsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsSelectOptionsCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj; + const stateObj = this._stateObj; const options = this._getOptions( - this.stateObj.attributes.options, + this._stateObj.attributes.options, this._config.options ); @@ -133,7 +158,7 @@ class HuiSelectOptionsCardFeature .label=${this.hass.localize("ui.card.select.option")} .value=${stateObj.state} .options=${options} - .disabled=${this.stateObj.state === UNAVAILABLE} + .disabled=${this._stateObj.state === UNAVAILABLE} fixedMenuPosition naturalMenuWidth @selected=${this._valueChanged} diff --git a/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts b/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts index f4650a66ad..1bef28543d 100644 --- a/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-target-humidity-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -9,9 +8,19 @@ import type { HumidifierEntity } from "../../../data/humidifier"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { TargetHumidityCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + TargetHumidityCardFeatureConfig, +} from "./types"; -export const supportsTargetHumidityCardFeature = (stateObj: HassEntity) => { +export const supportsTargetHumidityCardFeature = ( + 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 === "humidifier"; }; @@ -23,12 +32,21 @@ class HuiTargetHumidityCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HumidifierEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: TargetHumidityCardFeatureConfig; @state() private _targetHumidity?: number; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | HumidifierEntity + | undefined; + } + static getStubConfig(): TargetHumidityCardFeatureConfig { return { type: "target-humidity", @@ -44,19 +62,26 @@ class HuiTargetHumidityCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj")) { - this._targetHumidity = this.stateObj!.attributes.humidity; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._targetHumidity = this._stateObj!.attributes.humidity; + } } } private _step = 1; private get _min() { - return this.stateObj!.attributes.min_humidity ?? 0; + return this._stateObj!.attributes.min_humidity ?? 0; } private get _max() { - return this.stateObj!.attributes.max_humidity ?? 100; + return this._stateObj!.attributes.max_humidity ?? 100; } private _valueChanged(ev: CustomEvent) { @@ -68,7 +93,7 @@ class HuiTargetHumidityCardFeature private _callService() { this.hass!.callService("humidifier", "set_humidity", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, humidity: this._targetHumidity, }); } @@ -77,21 +102,25 @@ class HuiTargetHumidityCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsTargetHumidityCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsTargetHumidityCardFeature(this.hass, this.context) ) { return nothing; } return html` diff --git a/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts b/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts index 0cea86d0aa..1c7d763f93 100644 --- a/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-target-temperature-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues } from "lit"; import { html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -19,11 +18,21 @@ import { WaterHeaterEntityFeature } from "../../../data/water_heater"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { TargetTemperatureCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + TargetTemperatureCardFeatureConfig, +} from "./types"; type Target = "value" | "low" | "high"; -export const supportsTargetTemperatureCardFeature = (stateObj: HassEntity) => { +export const supportsTargetTemperatureCardFeature = ( + 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 === "climate" && @@ -44,14 +53,22 @@ class HuiTargetTemperatureCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: - | ClimateEntity - | WaterHeaterEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: TargetTemperatureCardFeatureConfig; @state() private _targetTemperature: Partial> = {}; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | WaterHeaterEntity + | ClimateEntity + | undefined; + } + static getStubConfig(): TargetTemperatureCardFeatureConfig { return { type: "target-temperature", @@ -67,34 +84,41 @@ class HuiTargetTemperatureCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj")) { - this._targetTemperature = { - value: this.stateObj!.attributes.temperature, - low: - "target_temp_low" in this.stateObj!.attributes - ? this.stateObj!.attributes.target_temp_low - : undefined, - high: - "target_temp_high" in this.stateObj!.attributes - ? this.stateObj!.attributes.target_temp_high - : undefined, - }; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._targetTemperature = { + value: this._stateObj!.attributes.temperature, + low: + "target_temp_low" in this._stateObj!.attributes + ? this._stateObj!.attributes.target_temp_low + : undefined, + high: + "target_temp_high" in this._stateObj!.attributes + ? this._stateObj!.attributes.target_temp_high + : undefined, + }; + } } } private get _step() { return ( - this.stateObj!.attributes.target_temp_step || + this._stateObj!.attributes.target_temp_step || (this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5) ); } private get _min() { - return this.stateObj!.attributes.min_temp; + return this._stateObj!.attributes.min_temp; } private get _max() { - return this.stateObj!.attributes.max_temp; + return this._stateObj!.attributes.max_temp; } private async _valueChanged(ev: CustomEvent) { @@ -115,43 +139,43 @@ class HuiTargetTemperatureCardFeature ); private _callService(type: string) { - const domain = computeStateDomain(this.stateObj!); + const domain = computeStateDomain(this._stateObj!); if (type === "high" || type === "low") { this.hass!.callService(domain, "set_temperature", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, target_temp_low: this._targetTemperature.low, target_temp_high: this._targetTemperature.high, }); return; } this.hass!.callService(domain, "set_temperature", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, temperature: this._targetTemperature.value, }); } private _supportsTarget() { - const domain = computeStateDomain(this.stateObj!); + const domain = computeStateDomain(this._stateObj!); return ( (domain === "climate" && supportsFeature( - this.stateObj!, + this._stateObj!, ClimateEntityFeature.TARGET_TEMPERATURE )) || (domain === "water_heater" && supportsFeature( - this.stateObj!, + this._stateObj!, WaterHeaterEntityFeature.TARGET_TEMPERATURE )) ); } private _supportsTargetRange() { - const domain = computeStateDomain(this.stateObj!); + const domain = computeStateDomain(this._stateObj!); return ( domain === "climate" && supportsFeature( - this.stateObj!, + this._stateObj!, ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) ); @@ -161,13 +185,14 @@ class HuiTargetTemperatureCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsTargetTemperatureCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsTargetTemperatureCardFeature(this.hass, this.context) ) { return nothing; } - const stateColor = stateColorCss(this.stateObj); + const stateColor = stateColorCss(this._stateObj); const digits = this._step.toString().split(".")?.[1]?.length ?? 0; const options = { @@ -178,27 +203,27 @@ class HuiTargetTemperatureCardFeature if ( this._supportsTarget() && this._targetTemperature.value != null && - this.stateObj.state !== UNAVAILABLE + this._stateObj.state !== UNAVAILABLE ) { return html` @@ -210,7 +235,7 @@ class HuiTargetTemperatureCardFeature this._supportsTargetRange() && this._targetTemperature.low != null && this._targetTemperature.high != null && - this.stateObj.state !== UNAVAILABLE + this._stateObj.state !== UNAVAILABLE ) { return html` @@ -227,13 +252,13 @@ class HuiTargetTemperatureCardFeature .step=${this._step} @value-changed=${this._valueChanged} .label=${this.hass.formatEntityAttributeName( - this.stateObj, + this._stateObj, "target_temp_low" )} style=${styleMap({ "--control-number-buttons-focus-color": stateColor, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .locale=${this.hass.locale} > @@ -250,13 +275,13 @@ class HuiTargetTemperatureCardFeature .step=${this._step} @value-changed=${this._valueChanged} .label=${this.hass.formatEntityAttributeName( - this.stateObj, + this._stateObj, "target_temp_high" )} style=${styleMap({ "--control-number-buttons-focus-color": stateColor, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} .locale=${this.hass.locale} > @@ -267,10 +292,10 @@ class HuiTargetTemperatureCardFeature return html` { +export const supportsToggleCardFeature = ( + 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 [ "switch", @@ -56,10 +66,17 @@ const DOMAIN_ICONS: Record = { class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: ToggleCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as HassEntity | undefined; + } + static getStubConfig(): ToggleCardFeatureConfig { return { type: "toggle", @@ -92,16 +109,16 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature { } private async _callService(turnOn): Promise { - if (!this.hass || !this.stateObj) { + if (!this.hass || !this._stateObj) { return; } forwardHaptic("light"); - const stateDomain = computeDomain(this.stateObj.entity_id); + const stateDomain = computeDomain(this._stateObj.entity_id); const serviceDomain = stateDomain; const service = turnOn ? "turn_on" : "turn_off"; await this.hass.callService(serviceDomain, service, { - entity_id: this.stateObj.entity_id, + entity_id: this._stateObj.entity_id, }); } @@ -109,32 +126,33 @@ class HuiToggleCardFeature extends LitElement implements LovelaceCardFeature { if ( !this._config || !this.hass || - !this.stateObj || - !supportsToggleCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsToggleCardFeature(this.hass, this.context) ) { return nothing; } const onColor = "var(--feature-color)"; - const offColor = stateColorCss(this.stateObj, "off"); + const offColor = stateColorCss(this._stateObj, "off"); - const isOn = this.stateObj.state === "on"; - const isOff = this.stateObj.state === "off"; + const isOn = this._stateObj.state === "on"; + const isOff = this._stateObj.state === "off"; - const domain = computeDomain(this.stateObj.entity_id); + const domain = computeDomain(this._stateObj.entity_id); const onIcon = DOMAIN_ICONS[domain]?.on || mdiPower; const offIcon = DOMAIN_ICONS[domain]?.off || mdiPowerOff; if ( - this.stateObj.attributes.assumed_state || - this.stateObj.state === UNKNOWN + this._stateObj.attributes.assumed_state || + this._stateObj.state === UNKNOWN ) { return html` `; diff --git a/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts b/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts index 1956df515a..80383e30eb 100644 --- a/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-update-actions-card-feature.ts @@ -1,5 +1,4 @@ import { mdiCancel, mdiCellphoneArrowDown } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; @@ -14,11 +13,21 @@ import { showUpdateBackupDialogParams } from "../../../dialogs/update_backup/sho import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { UpdateActionsCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + UpdateActionsCardFeatureConfig, +} from "./types"; export const DEFAULT_UPDATE_BACKUP_OPTION = "no"; -export const supportsUpdateActionsCardFeature = (stateObj: HassEntity) => { +export const supportsUpdateActionsCardFeature = ( + 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 === "update" && @@ -33,10 +42,19 @@ class HuiUpdateActionsCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: UpdateActionsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | UpdateEntity + | undefined; + } + public static async getConfigElement(): Promise { await import( "../editor/config-elements/hui-update-actions-card-feature-editor" @@ -59,7 +77,7 @@ class HuiUpdateActionsCardFeature } private get _installDisabled(): boolean { - const stateObj = this.stateObj as UpdateEntity; + const stateObj = this._stateObj as UpdateEntity; if (stateObj.state === UNAVAILABLE) return true; @@ -74,7 +92,7 @@ class HuiUpdateActionsCardFeature } private get _skipDisabled(): boolean { - const stateObj = this.stateObj as UpdateEntity; + const stateObj = this._stateObj as UpdateEntity; if (stateObj.state === UNAVAILABLE) return true; @@ -89,7 +107,7 @@ class HuiUpdateActionsCardFeature private async _install(): Promise { const supportsBackup = supportsFeature( - this.stateObj!, + this._stateObj!, UpdateEntityFeature.BACKUP ); let backup = supportsBackup && this._config?.backup === "yes"; @@ -101,14 +119,14 @@ class HuiUpdateActionsCardFeature } this.hass!.callService("update", "install", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, backup: backup, }); } private async _skip(): Promise { this.hass!.callService("update", "skip", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -116,8 +134,9 @@ class HuiUpdateActionsCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsUpdateActionsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsUpdateActionsCardFeature(this.hass, this.context) ) { return nothing; } diff --git a/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts b/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts index 20f77d12ca..433993ab21 100644 --- a/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-vacuum-commands-card-feature.ts @@ -13,8 +13,8 @@ import { customElement, property, state } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; import "../../../components/ha-control-button"; -import "../../../components/ha-svg-icon"; import "../../../components/ha-control-button-group"; +import "../../../components/ha-svg-icon"; import { UNAVAILABLE } from "../../../data/entity"; import type { VacuumEntity } from "../../../data/vacuum"; import { @@ -27,7 +27,11 @@ import { import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; -import type { VacuumCommand, VacuumCommandsCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + VacuumCommand, + VacuumCommandsCardFeatureConfig, +} from "./types"; import { VACUUM_COMMANDS } from "./types"; interface VacuumButton { @@ -115,7 +119,14 @@ export const VACUUM_COMMANDS_BUTTONS: Record< }), }; -export const supportsVacuumCommandsCardFeature = (stateObj: HassEntity) => { +export const supportsVacuumCommandsCardFeature = ( + 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 === "vacuum" && @@ -130,14 +141,26 @@ class HuiVacuumCommandCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: VacuumCommandsCardFeatureConfig; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | VacuumEntity + | undefined; + } + static getStubConfig( - _, - stateObj?: HassEntity + hass: HomeAssistant, + context: LovelaceCardFeatureContext ): VacuumCommandsCardFeatureConfig { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; return { type: "vacuum-commands", commands: stateObj @@ -166,7 +189,7 @@ class HuiVacuumCommandCardFeature ev.stopPropagation(); const entry = (ev.target! as any).entry as VacuumButton; this.hass!.callService("vacuum", entry.serviceName, { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, }); } @@ -174,13 +197,14 @@ class HuiVacuumCommandCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsVacuumCommandsCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsVacuumCommandsCardFeature(this.hass, this.context) ) { return nothing; } - const stateObj = this.stateObj as VacuumEntity; + const stateObj = this._stateObj as VacuumEntity; return html` diff --git a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts index a74041cbdd..2f61d1ac95 100644 --- a/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-water-heater-operation-modes-card-feature.ts @@ -1,4 +1,3 @@ -import type { HassEntity } from "home-assistant-js-websocket"; import type { PropertyValues, TemplateResult } from "lit"; import { html, LitElement } from "lit"; import { customElement, property, state } from "lit/decorators"; @@ -23,11 +22,19 @@ import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { cardFeatureStyles } from "./common/card-feature-styles"; import { filterModes } from "./common/filter-modes"; -import type { WaterHeaterOperationModesCardFeatureConfig } from "./types"; +import type { + LovelaceCardFeatureContext, + WaterHeaterOperationModesCardFeatureConfig, +} from "./types"; export const supportsWaterHeaterOperationModesCardFeature = ( - stateObj: HassEntity + 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 === "water_heater"; }; @@ -39,12 +46,21 @@ class HuiWaterHeaterOperationModeCardFeature { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: WaterHeaterEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @state() private _config?: WaterHeaterOperationModesCardFeatureConfig; @state() _currentOperationMode?: OperationMode; + private get _stateObj() { + if (!this.hass || !this.context || !this.context.entity_id) { + return undefined; + } + return this.hass.states[this.context.entity_id!] as + | WaterHeaterEntity + | undefined; + } + static getStubConfig(): WaterHeaterOperationModesCardFeatureConfig { return { type: "water-heater-operation-modes", @@ -69,17 +85,24 @@ class HuiWaterHeaterOperationModeCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); - if (changedProp.has("stateObj") && this.stateObj) { - this._currentOperationMode = this.stateObj.state as OperationMode; + if ( + (changedProp.has("hass") || changedProp.has("context")) && + this._stateObj + ) { + const oldHass = changedProp.get("hass") as HomeAssistant | undefined; + const oldStateObj = oldHass?.states[this.context!.entity_id!]; + if (oldStateObj !== this._stateObj) { + this._currentOperationMode = this._stateObj.state as OperationMode; + } } } private async _valueChanged(ev: CustomEvent) { const mode = (ev.detail as any).value as OperationMode; - if (mode === this.stateObj!.state) return; + if (mode === this._stateObj!.state) return; - const oldMode = this.stateObj!.state as OperationMode; + const oldMode = this._stateObj!.state as OperationMode; this._currentOperationMode = mode; try { @@ -91,7 +114,7 @@ class HuiWaterHeaterOperationModeCardFeature private async _setMode(mode: OperationMode) { await this.hass!.callService("water_heater", "set_operation_mode", { - entity_id: this.stateObj!.entity_id, + entity_id: this._stateObj!.entity_id, operation_mode: mode, }); } @@ -100,15 +123,16 @@ class HuiWaterHeaterOperationModeCardFeature if ( !this._config || !this.hass || - !this.stateObj || - !supportsWaterHeaterOperationModesCardFeature(this.stateObj) + !this.context || + !this._stateObj || + !supportsWaterHeaterOperationModesCardFeature(this.hass, this.context) ) { return null; } - const color = stateColorCss(this.stateObj); + const color = stateColorCss(this._stateObj); - const orderedModes = (this.stateObj.attributes.operation_list || []) + const orderedModes = (this._stateObj.attributes.operation_list || []) .concat() .sort(compareWaterHeaterOperationMode) .reverse(); @@ -118,7 +142,7 @@ class HuiWaterHeaterOperationModeCardFeature this._config.operation_modes ).map((mode) => ({ value: mode, - label: this.hass!.formatEntityState(this.stateObj!, mode), + label: this.hass!.formatEntityState(this._stateObj!, mode), path: computeOperationModeIcon(mode as OperationMode), })); @@ -132,7 +156,7 @@ class HuiWaterHeaterOperationModeCardFeature style=${styleMap({ "--control-select-color": color, })} - .disabled=${this.stateObj!.state === UNAVAILABLE} + .disabled=${this._stateObj!.state === UNAVAILABLE} > `; diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts index 5ed6ad3e3a..761369d350 100644 --- a/src/panels/lovelace/cards/hui-humidifier-card.ts +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -14,6 +14,7 @@ import type { HumidifierEntity } from "../../../data/humidifier"; 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 { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { @@ -69,6 +70,8 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { @state() private _config?: HumidifierCardConfig; + @state() private _featureContext: LovelaceCardFeatureContext = {}; + public getCardSize(): number { return 7; } @@ -79,6 +82,9 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { } this._config = config; + this._featureContext = { + entity_id: config.entity, + }; } private _handleMoreInfo() { @@ -165,7 +171,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { "--feature-color": color, })} .hass=${this.hass} - .stateObj=${stateObj} + .context=${this._featureContext} .features=${this._config.features} >` : nothing} diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index 3e6853e405..a97e95d980 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -14,6 +14,7 @@ import type { ClimateEntity } from "../../../data/climate"; import "../../../state-control/climate/ha-state-control-climate-temperature"; import type { HomeAssistant } from "../../../types"; import "../card-features/hui-card-features"; +import type { LovelaceCardFeatureContext } from "../card-features/types"; import { findEntities } from "../common/find-entities"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import type { @@ -61,6 +62,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { @state() private _config?: ThermostatCardConfig; + @state() private _featureContext: LovelaceCardFeatureContext = {}; + public getCardSize(): number { return 7; } @@ -71,6 +74,9 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { } this._config = config; + this._featureContext = { + entity_id: config.entity, + }; } private _handleMoreInfo() { @@ -157,7 +163,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { "--feature-color": color, })} .hass=${this.hass} - .stateObj=${stateObj} + .context=${this._featureContext} .features=${this._config.features} >` : nothing} diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts index d0c48dc8b4..869ef18003 100644 --- a/src/panels/lovelace/cards/hui-tile-card.ts +++ b/src/panels/lovelace/cards/hui-tile-card.ts @@ -36,6 +36,7 @@ import type { } from "../types"; import { renderTileBadge } from "./tile/badges/tile-badge"; import type { TileCardConfig } from "./types"; +import type { LovelaceCardFeatureContext } from "../card-features/types"; import { createEntityNotFoundWarning } from "../components/hui-warning"; export const getEntityDefaultTileIconAction = (entityId: string) => { @@ -84,6 +85,8 @@ export class HuiTileCard extends LitElement implements LovelaceCard { @state() private _config?: TileCardConfig; + @state() private _featureContext: LovelaceCardFeatureContext = {}; + public setConfig(config: TileCardConfig): void { if (!config.entity) { throw new Error("Specify an entity"); @@ -98,6 +101,9 @@ export class HuiTileCard extends LitElement implements LovelaceCard { }, ...config, }; + this._featureContext = { + entity_id: config.entity, + }; } public getCardSize(): number { @@ -335,7 +341,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { ? html` 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 18ffbd4276..b92723ecc2 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 @@ -1,5 +1,4 @@ import { mdiDelete, mdiDrag, mdiPencil, mdiPlus } from "@mdi/js"; -import type { HassEntity } from "home-assistant-js-websocket"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; @@ -22,8 +21,8 @@ import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-mod import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature"; import { supportsClimateHvacModesCardFeature } from "../../card-features/hui-climate-hvac-modes-card-feature"; import { supportsClimatePresetModesCardFeature } from "../../card-features/hui-climate-preset-modes-card-feature"; -import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature"; import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-features/hui-climate-swing-horizontal-modes-card-feature"; +import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature"; import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature"; import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature"; import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature"; @@ -47,11 +46,18 @@ import { supportsToggleCardFeature } from "../../card-features/hui-toggle-card-f import { supportsUpdateActionsCardFeature } from "../../card-features/hui-update-actions-card-feature"; import { supportsVacuumCommandsCardFeature } from "../../card-features/hui-vacuum-commands-card-feature"; import { supportsWaterHeaterOperationModesCardFeature } from "../../card-features/hui-water-heater-operation-modes-card-feature"; -import type { LovelaceCardFeatureConfig } from "../../card-features/types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "../../card-features/types"; import { getCardFeatureElementClass } from "../../create-element/create-card-feature-element"; export type FeatureType = LovelaceCardFeatureConfig["type"]; -type SupportsFeature = (stateObj: HassEntity) => boolean; + +type SupportsFeature = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext +) => boolean; const UI_FEATURE_TYPES = [ "alarm-modes", @@ -152,7 +158,8 @@ customCardFeatures.forEach((feature) => { }); export const getSupportedFeaturesType = ( - stateObj: HassEntity, + hass: HomeAssistant, + context: LovelaceCardFeatureContext, featuresTypes?: string[] ) => { const filteredFeaturesTypes = UI_FEATURE_TYPES.filter( @@ -164,23 +171,41 @@ export const getSupportedFeaturesType = ( ); return filteredFeaturesTypes .concat(customFeaturesTypes) - .filter((type) => supportsFeaturesType(stateObj, type)); + .filter((type) => supportsFeaturesType(hass, context, type)); }; -export const supportsFeaturesType = (stateObj: HassEntity, type: string) => { +export const supportsFeaturesType = ( + hass: HomeAssistant, + context: LovelaceCardFeatureContext, + type: string +) => { if (isCustomType(type)) { const customType = stripCustomPrefix(type); const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType]; - if (!customFeatureEntry?.supported) return true; + + if (!customFeatureEntry) { + return false; + } try { - return customFeatureEntry.supported(stateObj); + if (customFeatureEntry.isSupported) { + return customFeatureEntry.isSupported(hass, context); + } + // Fallback to the old supported method + if (customFeatureEntry.supported) { + const stateObj = context.entity_id + ? hass.states[context.entity_id] + : undefined; + if (!stateObj) return false; + return customFeatureEntry.supported(stateObj); + } + return true; } catch { return false; } } const supportsFeature = SUPPORTS_FEATURE_TYPES[type]; - return !supportsFeature || supportsFeature(stateObj); + return !supportsFeature || supportsFeature(hass, context); }; declare global { @@ -195,7 +220,7 @@ declare global { export class HuiCardFeaturesEditor extends LitElement { @property({ attribute: false }) public hass?: HomeAssistant; - @property({ attribute: false }) public stateObj?: HassEntity; + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; @property({ attribute: false }) public features?: LovelaceCardFeatureConfig[]; @@ -209,13 +234,17 @@ export class HuiCardFeaturesEditor extends LitElement { private _featuresKeys = new WeakMap(); private _supportsFeatureType(type: string): boolean { - if (!this.stateObj) return false; - return supportsFeaturesType(this.stateObj, type); + if (!this.hass || !this.context) return false; + return supportsFeaturesType(this.hass, this.context, type); } private _getSupportedFeaturesType() { - if (!this.stateObj) return []; - return getSupportedFeaturesType(this.stateObj, this.featuresTypes); + if (!this.hass || !this.context) return []; + return getSupportedFeaturesType( + this.hass, + this.context, + this.featuresTypes + ); } private _isFeatureTypeEditable(type: string) { @@ -288,7 +317,7 @@ export class HuiCardFeaturesEditor extends LitElement {
${this._getFeatureTypeLabel(type)} - ${this.stateObj && !supported + ${this.context && !supported ? html` ${this.hass!.localize( @@ -379,7 +408,14 @@ export class HuiCardFeaturesEditor extends LitElement { let newFeature: LovelaceCardFeatureConfig; if (elClass && elClass.getStubConfig) { - newFeature = await elClass.getStubConfig(this.hass!, this.stateObj); + try { + newFeature = await elClass.getStubConfig(this.hass!, this.context!); + } catch (_err) { + const stateObj = this.context!.entity_id + ? this.hass!.states[this.context!.entity_id] + : undefined; + newFeature = await elClass.getStubConfig(this.hass!, stateObj); + } } else { newFeature = { type: value } as LovelaceCardFeatureConfig; } diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts index ebc035dfe3..e110fa8033 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -1,6 +1,7 @@ import { mdiListBox } from "@mdi/js"; import { LitElement, css, html, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { any, array, @@ -85,13 +86,19 @@ export class HuiHumidifierCardEditor this._config = config; } + private _featureContext = memoizeOne( + (entityId?: string): LovelaceCardFeatureContext => ({ + entity_id: entityId, + }) + ); + protected render() { if (!this.hass || !this._config) { return nothing; } - const entityId = this._config!.entity; - const stateObj = entityId ? this.hass!.states[entityId] : undefined; + const entityId = this._config.entity; + const featureContext = this._featureContext(entityId); return html` ({ + entity_id: entityId, + }) + ); + protected render() { if (!this.hass || !this._config) { return nothing; } - const entityId = this._config!.entity; - const stateObj = entityId ? this.hass!.states[entityId] : undefined; + const entityId = this._config.entity; + const featureContext = this._featureContext(entityId); return html` ({ + entity_id: entityId, + }) + ); + private _schema = memoizeOne( ( localize: LocalizeFunc, @@ -239,7 +244,8 @@ export class HuiTileCardEditor ); private _hasCompatibleFeatures = memoizeOne( - (stateObj: HassEntity) => getSupportedFeaturesType(stateObj).length > 0 + (context: LovelaceCardFeatureContext) => + getSupportedFeaturesType(this.hass!, context).length > 0 ); protected render() { @@ -248,7 +254,6 @@ export class HuiTileCardEditor } const entityId = this._config!.entity; - const stateObj = entityId ? this.hass!.states[entityId] : undefined; const schema = this._schema( this.hass.localize, @@ -271,8 +276,8 @@ export class HuiTileCardEditor data.features_position = "bottom"; } - const hasCompatibleFeatures = - (stateObj && this._hasCompatibleFeatures(stateObj)) || false; + const featureContext = this._featureContext(entityId); + const hasCompatibleFeatures = this._hasCompatibleFeatures(featureContext); return html` ): void { const index = ev.detail.subElementConfig.index; const config = this._config!.features![index!]; + const featureContext = this._featureContext(this._config!.entity); fireEvent(this, "edit-sub-element", { config: config, saveConfig: (newConfig) => this._updateFeature(index!, newConfig), - context: { - entity_id: this._config!.entity, - }, + context: featureContext, type: "feature", } as EditSubElementEvent< LovelaceCardFeatureConfig, diff --git a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts index 065393e258..cbebee032b 100644 --- a/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts +++ b/src/panels/lovelace/strategies/areas/helpers/areas-strategy-helper.ts @@ -13,7 +13,10 @@ import { supportsCoverOpenCloseCardFeature } from "../../../card-features/hui-co import { supportsLightBrightnessCardFeature } from "../../../card-features/hui-light-brightness-card-feature"; import { supportsLockCommandsCardFeature } from "../../../card-features/hui-lock-commands-card-feature"; import { supportsTargetTemperatureCardFeature } from "../../../card-features/hui-target-temperature-card-feature"; -import type { LovelaceCardFeatureConfig } from "../../../card-features/types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "../../../card-features/types"; import type { TileCardConfig } from "../../../cards/types"; export const AREA_STRATEGY_GROUPS = [ @@ -206,6 +209,10 @@ export const computeAreaTileCardConfig = (entity: string): LovelaceCardConfig => { const stateObj = hass.states[entity]; + const context: LovelaceCardFeatureContext = { + entity_id: entity, + }; + const additionalCardConfig: Partial = {}; const domain = computeDomain(entity); @@ -225,23 +232,23 @@ export const computeAreaTileCardConfig = let feature: LovelaceCardFeatureConfig | undefined; if (includeFeature) { - if (supportsLightBrightnessCardFeature(stateObj)) { + if (supportsLightBrightnessCardFeature(hass, context)) { feature = { type: "light-brightness", }; - } else if (supportsCoverOpenCloseCardFeature(stateObj)) { + } else if (supportsCoverOpenCloseCardFeature(hass, context)) { feature = { type: "cover-open-close", }; - } else if (supportsTargetTemperatureCardFeature(stateObj)) { + } else if (supportsTargetTemperatureCardFeature(hass, context)) { feature = { type: "target-temperature", }; - } else if (supportsAlarmModesCardFeature(stateObj)) { + } else if (supportsAlarmModesCardFeature(hass, context)) { feature = { type: "alarm-modes", }; - } else if (supportsLockCommandsCardFeature(stateObj)) { + } else if (supportsLockCommandsCardFeature(hass, context)) { feature = { type: "lock-commands", }; diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts index c3810265d9..8da041bb27 100644 --- a/src/panels/lovelace/types.ts +++ b/src/panels/lovelace/types.ts @@ -8,13 +8,16 @@ import type { LovelaceRawConfig, } from "../../data/lovelace/config/types"; import type { FrontendLocaleData } from "../../data/translation"; +import type { ShowToastParams } from "../../managers/notification-manager"; import type { Constructor, HomeAssistant } from "../../types"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./card-features/types"; +import type { LovelaceElement, LovelaceElementConfig } from "./elements/types"; import type { LovelaceRow, LovelaceRowConfig } from "./entity-rows/types"; import type { LovelaceHeaderFooterConfig } from "./header-footer/types"; -import type { LovelaceCardFeatureConfig } from "./card-features/types"; -import type { LovelaceElement, LovelaceElementConfig } from "./elements/types"; import type { LovelaceHeadingBadgeConfig } from "./heading-badges/types"; -import type { ShowToastParams } from "../../managers/notification-manager"; declare global { interface HASSDomEvents { @@ -169,7 +172,9 @@ export interface LovelaceGenericElementEditor extends HTMLElement { export interface LovelaceCardFeature extends HTMLElement { hass?: HomeAssistant; + /** @deprecated Use `context` instead */ stateObj?: HassEntity; + context?: LovelaceCardFeatureContext; setConfig(config: LovelaceCardFeatureConfig); color?: string; } @@ -178,7 +183,7 @@ export interface LovelaceCardFeatureConstructor extends Constructor { getStubConfig?: ( hass: HomeAssistant, - stateObj?: HassEntity + context?: LovelaceCardFeatureContext ) => LovelaceCardFeatureConfig; getConfigElement?: () => LovelaceCardFeatureEditor; getConfigForm?: () => {