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 fc03ed3fbd..d347363fe2 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,21 +1,12 @@ import { mdiFan } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; -import { UNAVAILABLE } from "../../../data/entity/entity"; 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 { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { ClimateFanModesCardFeatureConfig, LovelaceCardFeatureContext, @@ -38,34 +29,26 @@ export const supportsClimateFanModesCardFeature = ( @customElement("hui-climate-fan-modes-card-feature") class HuiClimateFanModesCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + ClimateEntity, + ClimateFanModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "fan_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "fan_modes"; - @state() private _config?: ClimateFanModesCardFeatureConfig; - - @state() _currentFanMode?: string; - - private _renderFanModeIcon = (value: string) => - html``; - - 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; + protected get _configuredModes() { + return this._config?.fan_modes; } + protected readonly _dropdownIconPath = mdiFan; + + protected readonly _serviceDomain = "climate"; + + protected readonly _serviceAction = "set_fan_mode"; + static getStubConfig(): ClimateFanModesCardFeatureConfig { return { type: "climate-fan-modes", @@ -78,120 +61,12 @@ class HuiClimateFanModesCardFeature return document.createElement("hui-climate-fan-modes-card-feature-editor"); } - public setConfig(config: ClimateFanModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } - } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const fanMode = ev.detail.value ?? ev.detail.item?.value; - - const oldFanMode = this._stateObj!.attributes.fan_mode; - - if (fanMode === oldFanMode || !fanMode) { - return; - } - - this._currentFanMode = fanMode; - - try { - await this._setMode(fanMode); - } catch (_err) { - this._currentFanMode = oldFanMode; - } - } - - private async _setMode(mode: string) { - await this.hass!.callService("climate", "set_fan_mode", { - entity_id: this._stateObj!.entity_id, - fan_mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsClimateFanModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const stateObj = this._stateObj; - - const options = filterModes( - stateObj.attributes.fan_modes, - this._config!.fan_modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "fan_mode", - mode - ), - })); - - if (this._config.style === "icons") { - return html` - ({ - ...option, - icon: html``, - }))} - .value=${this._currentFanMode} - @value-changed=${this._valueChanged} - hide-option-label - .label=${this.hass!.formatEntityAttributeName(stateObj, "fan_mode")} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; - } - - return html` - - - `; - } - - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsClimateFanModesCardFeature(this.hass, this.context) + ); } } 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 202f4d5aa5..bfc00a441d 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,28 +1,30 @@ import { mdiThermostat } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; +import type { TemplateResult } from "lit"; +import { html } from "lit"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateColorCss } from "../../../common/entity/state_color"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; -import type { ClimateEntity, HvacMode } from "../../../data/climate"; +import type { ClimateEntity } from "../../../data/climate"; import { climateHvacModeIcon, compareClimateHvacModes, } from "../../../data/climate"; -import { UNAVAILABLE } from "../../../data/entity/entity"; 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 { + HuiModeSelectCardFeatureBase, + type HuiModeSelectOption, +} from "./hui-mode-select-card-feature-base"; import type { ClimateHvacModesCardFeatureConfig, LovelaceCardFeatureContext, } from "./types"; +interface HvacModeOption extends HuiModeSelectOption { + iconPath: string; +} + export const supportsClimateHvacModesCardFeature = ( hass: HomeAssistant, context: LovelaceCardFeatureContext @@ -37,24 +39,44 @@ export const supportsClimateHvacModesCardFeature = ( @customElement("hui-climate-hvac-modes-card-feature") class HuiClimateHvacModesCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + ClimateEntity, + ClimateHvacModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "hvac_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "hvac_modes"; - @state() private _config?: ClimateHvacModesCardFeatureConfig; + protected get _configuredModes() { + return this._config?.hvac_modes; + } - @state() _currentHvacMode?: HvacMode; + protected readonly _dropdownIconPath = mdiThermostat; - private get _stateObj() { - if (!this.hass || !this.context || !this.context.entity_id) { + protected readonly _serviceDomain = "climate"; + + protected readonly _serviceAction = "set_hvac_mode"; + + protected get _label(): string { + return this.hass!.localize("ui.card.climate.mode"); + } + + protected readonly _showDropdownOptionIcons = false; + + protected readonly _defaultStyle = "icons"; + + protected get _controlSelectStyle(): + | Record + | undefined { + if (!this._stateObj) { return undefined; } - return this.hass.states[this.context.entity_id!] as - | ClimateEntity - | undefined; + + return { + "--control-select-color": stateColorCss(this._stateObj), + }; } static getStubConfig(): ClimateHvacModesCardFeatureConfig { @@ -68,119 +90,42 @@ class HuiClimateHvacModesCardFeature return document.createElement("hui-climate-hvac-modes-card-feature-editor"); } - public setConfig(config: ClimateHvacModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; + protected _getValue(stateObj: ClimateEntity): string | undefined { + return stateObj.state; } - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } - } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const mode = ev.detail.value ?? ev.detail.item?.value; - - if (mode === this._stateObj!.state || !mode) { - return; + protected _getOptions(): HvacModeOption[] { + if (!this._stateObj || !this.hass) { + return []; } - const oldMode = this._stateObj!.state as HvacMode; - this._currentHvacMode = mode as HvacMode; - - try { - await this._setMode(this._currentHvacMode); - } catch (_err) { - this._currentHvacMode = oldMode; - } - } - - private async _setMode(mode: HvacMode) { - await this.hass!.callService("climate", "set_hvac_mode", { - entity_id: this._stateObj!.entity_id, - hvac_mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsClimateHvacModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const color = stateColorCss(this._stateObj); - - const ordererHvacModes = (this._stateObj.attributes.hvac_modes || []) + const orderedHvacModes = (this._stateObj.attributes.hvac_modes || []) .concat() .sort(compareClimateHvacModes) .reverse(); - const options = filterModes(ordererHvacModes, this._config.hvac_modes).map( + return filterModes(orderedHvacModes, this._config?.hvac_modes).map( (mode) => ({ - value: mode as string, + value: mode, label: this.hass!.formatEntityState(this._stateObj!, mode), iconPath: climateHvacModeIcon(mode), }) ); - - if (this._config.style === "dropdown") { - return html` - - - - `; - } - - return html` - ({ - ...option, - icon: html``, - }))} - .value=${this._currentHvacMode} - @value-changed=${this._valueChanged} - hide-option-label - .label=${this.hass.localize("ui.card.climate.mode")} - style=${styleMap({ - "--control-select-color": color, - })} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; } - static get styles() { - return cardFeatureStyles; + protected _renderOptionIcon(option: HvacModeOption): TemplateResult<1> { + return html``; + } + + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsClimateHvacModesCardFeature(this.hass, this.context) + ); } } 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 df92172317..69294c900f 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,20 +1,12 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; -import { UNAVAILABLE } from "../../../data/entity/entity"; 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 { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { ClimatePresetModesCardFeatureConfig, LovelaceCardFeatureContext, @@ -37,34 +29,26 @@ export const supportsClimatePresetModesCardFeature = ( @customElement("hui-climate-preset-modes-card-feature") class HuiClimatePresetModesCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + ClimateEntity, + ClimatePresetModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "preset_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "preset_modes"; - @state() private _config?: ClimatePresetModesCardFeatureConfig; - - @state() _currentPresetMode?: string; - - private _renderPresetModeIcon = (value: string) => - html``; - - 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; + protected get _configuredModes() { + return this._config?.preset_modes; } + protected readonly _dropdownIconPath = mdiTuneVariant; + + protected readonly _serviceDomain = "climate"; + + protected readonly _serviceAction = "set_preset_mode"; + static getStubConfig(): ClimatePresetModesCardFeatureConfig { return { type: "climate-preset-modes", @@ -79,124 +63,12 @@ class HuiClimatePresetModesCardFeature ); } - public setConfig(config: ClimatePresetModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } - } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const presetMode = ev.detail.value ?? ev.detail.item?.value; - - const oldPresetMode = this._stateObj!.attributes.preset_mode; - - if (presetMode === oldPresetMode || !presetMode) { - return; - } - - this._currentPresetMode = presetMode; - - try { - await this._setMode(presetMode); - } catch (_err) { - this._currentPresetMode = oldPresetMode; - } - } - - private async _setMode(mode: string) { - await this.hass!.callService("climate", "set_preset_mode", { - entity_id: this._stateObj!.entity_id, - preset_mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsClimatePresetModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const stateObj = this._stateObj; - - const options = filterModes( - stateObj.attributes.preset_modes, - this._config!.preset_modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "preset_mode", - mode - ), - })); - - if (this._config.style === "icons") { - return html` - ({ - ...option, - icon: html``, - }))} - .value=${this._currentPresetMode} - @value-changed=${this._valueChanged} - hide-option-label - .label=${this.hass!.formatEntityAttributeName( - stateObj, - "preset_mode" - )} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; - } - - return html` - - - - `; - } - - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsClimatePresetModesCardFeature(this.hass, this.context) + ); } } 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 946dd32f95..f14f11ae5c 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,20 +1,12 @@ import { mdiArrowOscillating } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; -import { UNAVAILABLE } from "../../../data/entity/entity"; 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 { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { ClimateSwingHorizontalModesCardFeatureConfig, LovelaceCardFeatureContext, @@ -37,34 +29,26 @@ export const supportsClimateSwingHorizontalModesCardFeature = ( @customElement("hui-climate-swing-horizontal-modes-card-feature") class HuiClimateSwingHorizontalModesCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + ClimateEntity, + ClimateSwingHorizontalModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "swing_horizontal_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "swing_horizontal_modes"; - @state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig; - - @state() _currentSwingHorizontalMode?: string; - - private _renderSwingHorizontalModeIcon = (value: string) => - html``; - - 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; + protected get _configuredModes() { + return this._config?.swing_horizontal_modes; } + protected readonly _dropdownIconPath = mdiArrowOscillating; + + protected readonly _serviceDomain = "climate"; + + protected readonly _serviceAction = "set_swing_horizontal_mode"; + static getStubConfig(): ClimateSwingHorizontalModesCardFeatureConfig { return { type: "climate-swing-horizontal-modes", @@ -79,132 +63,12 @@ class HuiClimateSwingHorizontalModesCardFeature ); } - public setConfig(config: ClimateSwingHorizontalModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } - } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const swingHorizontalMode = ev.detail.value ?? ev.detail.item?.value; - - const oldSwingHorizontalMode = - this._stateObj!.attributes.swing_horizontal_mode; - - if ( - swingHorizontalMode === oldSwingHorizontalMode || - !swingHorizontalMode - ) { - return; - } - - this._currentSwingHorizontalMode = swingHorizontalMode; - - try { - await this._setMode(swingHorizontalMode); - } catch (_err) { - this._currentSwingHorizontalMode = oldSwingHorizontalMode; - } - } - - private async _setMode(mode: string) { - await this.hass!.callService("climate", "set_swing_horizontal_mode", { - entity_id: this._stateObj!.entity_id, - swing_horizontal_mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsClimateSwingHorizontalModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const stateObj = this._stateObj; - - const options = filterModes( - stateObj.attributes.swing_horizontal_modes, - this._config!.swing_horizontal_modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "swing_horizontal_mode", - mode - ), - })); - - if (this._config.style === "icons") { - return html` - ({ - ...option, - icon: html``, - }))} - .value=${this._currentSwingHorizontalMode} - @value-changed=${this._valueChanged} - hide-option-label - .label=${this.hass!.formatEntityAttributeName( - stateObj, - "swing_horizontal_mode" - )} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; - } - - return html` - - - - `; - } - - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsClimateSwingHorizontalModesCardFeature(this.hass, this.context) + ); } } 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 4b2c9d5ded..183ea75665 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,20 +1,12 @@ import { mdiArrowOscillating } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; import type { ClimateEntity } from "../../../data/climate"; import { ClimateEntityFeature } from "../../../data/climate"; -import { UNAVAILABLE } from "../../../data/entity/entity"; 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 { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { ClimateSwingModesCardFeatureConfig, LovelaceCardFeatureContext, @@ -37,34 +29,26 @@ export const supportsClimateSwingModesCardFeature = ( @customElement("hui-climate-swing-modes-card-feature") class HuiClimateSwingModesCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + ClimateEntity, + ClimateSwingModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "swing_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "swing_modes"; - @state() private _config?: ClimateSwingModesCardFeatureConfig; - - @state() _currentSwingMode?: string; - - private _renderSwingModeIcon = (value: string) => - html``; - - 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; + protected get _configuredModes() { + return this._config?.swing_modes; } + protected readonly _dropdownIconPath = mdiArrowOscillating; + + protected readonly _serviceDomain = "climate"; + + protected readonly _serviceAction = "set_swing_mode"; + static getStubConfig(): ClimateSwingModesCardFeatureConfig { return { type: "climate-swing-modes", @@ -79,123 +63,12 @@ class HuiClimateSwingModesCardFeature ); } - public setConfig(config: ClimateSwingModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } - } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const swingMode = ev.detail.value ?? ev.detail.item?.value; - - const oldSwingMode = this._stateObj!.attributes.swing_mode; - - if (swingMode === oldSwingMode || !swingMode) { - return; - } - - this._currentSwingMode = swingMode; - - try { - await this._setMode(swingMode); - } catch (_err) { - this._currentSwingMode = oldSwingMode; - } - } - - private async _setMode(mode: string) { - await this.hass!.callService("climate", "set_swing_mode", { - entity_id: this._stateObj!.entity_id, - swing_mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsClimateSwingModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const stateObj = this._stateObj; - - const options = filterModes( - stateObj.attributes.swing_modes, - this._config!.swing_modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "swing_mode", - mode - ), - })); - - if (this._config.style === "icons") { - return html` - ({ - ...option, - icon: html``, - }))} - .value=${this._currentSwingMode} - @value-changed=${this._valueChanged} - hide-option-label - .ariaLabel=${this.hass!.formatEntityAttributeName( - stateObj, - "swing_mode" - )} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; - } - - return html` - - - `; - } - - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsClimateSwingModesCardFeature(this.hass, this.context) + ); } } diff --git a/src/panels/lovelace/card-features/hui-fan-direction-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-direction-card-feature.ts index 6a714bbdd0..90ee61bd90 100644 --- a/src/panels/lovelace/card-features/hui-fan-direction-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-fan-direction-card-feature.ts @@ -1,22 +1,21 @@ -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; -import { UNAVAILABLE } from "../../../data/entity/entity"; import type { FanDirection, FanEntity } from "../../../data/fan"; import { FanEntityFeature } from "../../../data/fan"; import type { HomeAssistant } from "../../../types"; import type { LovelaceCardFeature } from "../types"; -import { cardFeatureStyles } from "./common/card-feature-styles"; +import { + HuiModeSelectCardFeatureBase, + type HuiModeSelectOption, +} from "./hui-mode-select-card-feature-base"; import type { FanDirectionCardFeatureConfig, LovelaceCardFeatureContext, } from "./types"; +const FAN_DIRECTIONS: FanDirection[] = ["forward", "reverse"]; + export const supportsFanDirectionCardFeature = ( hass: HomeAssistant, context: LovelaceCardFeatureContext @@ -33,23 +32,18 @@ export const supportsFanDirectionCardFeature = ( @customElement("hui-fan-direction-card-feature") class HuiFanDirectionCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "direction"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "direction"; - @state() private _config?: FanDirectionCardFeatureConfig; + protected readonly _serviceDomain = "fan"; - @state() _currentDirection?: FanDirection; + protected readonly _serviceAction = "set_direction"; - 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; - } + protected readonly _defaultStyle = "icons"; static getStubConfig(): FanDirectionCardFeatureConfig { return { @@ -57,90 +51,23 @@ class HuiFanDirectionCardFeature }; } - public setConfig(config: FanDirectionCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProp: PropertyValues): void { - 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._currentDirection = this._stateObj.attributes - .direction as FanDirection; - } - } - } - - private async _valueChanged(ev: CustomEvent) { - const newDirection = (ev.detail as any).value as FanDirection; - - if (newDirection === this._stateObj!.attributes.direction) return; - - const oldDirection = this._stateObj!.attributes.direction as FanDirection; - this._currentDirection = newDirection; - - try { - await this._setDirection(newDirection); - } catch (_err) { - this._currentDirection = oldDirection; - } - } - - private async _setDirection(direction: string) { - await this.hass!.callService("fan", "set_direction", { - entity_id: this._stateObj!.entity_id, - direction: direction, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsFanDirectionCardFeature(this.hass, this.context) - ) { - return null; + protected _getOptions(): HuiModeSelectOption[] { + if (!this.hass) { + return []; } - const stateObj = this._stateObj; - const FAN_DIRECTION_MAP: FanDirection[] = ["forward", "reverse"]; - - const options = FAN_DIRECTION_MAP.map((direction) => ({ + return FAN_DIRECTIONS.map((direction) => ({ value: direction, label: this.hass!.localize(`ui.card.fan.${direction}`), - icon: html``, })); - - return html` - - - `; } - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsFanDirectionCardFeature(this.hass, this.context) + ); } } 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 b63acb8aae..c804427117 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,20 +1,12 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; -import { UNAVAILABLE } from "../../../data/entity/entity"; import type { FanEntity } from "../../../data/fan"; import { FanEntityFeature } from "../../../data/fan"; 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 { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { FanPresetModesCardFeatureConfig, LovelaceCardFeatureContext, @@ -36,32 +28,26 @@ export const supportsFanPresetModesCardFeature = ( @customElement("hui-fan-preset-modes-card-feature") class HuiFanPresetModesCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + FanEntity, + FanPresetModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "preset_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "preset_modes"; - @state() private _config?: FanPresetModesCardFeatureConfig; - - @state() _currentPresetMode?: string; - - private _renderPresetModeIcon = (value: string) => - html``; - - 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; + protected get _configuredModes() { + return this._config?.preset_modes; } + protected readonly _dropdownIconPath = mdiTuneVariant; + + protected readonly _serviceDomain = "fan"; + + protected readonly _serviceAction = "set_preset_mode"; + static getStubConfig(): FanPresetModesCardFeatureConfig { return { type: "fan-preset-modes", @@ -74,123 +60,12 @@ class HuiFanPresetModesCardFeature return document.createElement("hui-fan-preset-modes-card-feature-editor"); } - public setConfig(config: FanPresetModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProp: PropertyValues): void { - 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; - } - } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const presetMode = ev.detail.value ?? ev.detail.item?.value; - - const oldPresetMode = this._stateObj!.attributes.preset_mode; - - if (presetMode === oldPresetMode || !presetMode) { - return; - } - - this._currentPresetMode = presetMode; - - try { - await this._setMode(presetMode); - } catch (_err) { - this._currentPresetMode = oldPresetMode; - } - } - - private async _setMode(mode: string) { - await this.hass!.callService("fan", "set_preset_mode", { - entity_id: this._stateObj!.entity_id, - preset_mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsFanPresetModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const stateObj = this._stateObj; - - const options = filterModes( - stateObj.attributes.preset_modes, - this._config!.preset_modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "preset_mode", - mode - ), - })); - - if (this._config.style === "icons") { - return html` - ({ - ...option, - icon: html``, - }))} - .value=${this._currentPresetMode} - @value-changed=${this._valueChanged} - hide-option-label - .label=${this.hass!.formatEntityAttributeName( - stateObj, - "preset_mode" - )} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; - } - - return html` - - - - `; - } - - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsFanPresetModesCardFeature(this.hass, this.context) + ); } } 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 f65b7582b9..e437eb73ee 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,20 +1,12 @@ import { mdiTuneVariant } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; -import { UNAVAILABLE } from "../../../data/entity/entity"; import type { HumidifierEntity } from "../../../data/humidifier"; import { HumidifierEntityFeature } from "../../../data/humidifier"; 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 { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { HumidifierModesCardFeatureConfig, LovelaceCardFeatureContext, @@ -37,34 +29,26 @@ export const supportsHumidifierModesCardFeature = ( @customElement("hui-humidifier-modes-card-feature") class HuiHumidifierModesCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + HumidifierEntity, + HumidifierModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "available_modes"; - @state() private _config?: HumidifierModesCardFeatureConfig; - - @state() _currentMode?: string; - - private _renderModeIcon = (value: string) => - html``; - - 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; + protected get _configuredModes() { + return this._config?.modes; } + protected readonly _dropdownIconPath = mdiTuneVariant; + + protected readonly _serviceDomain = "humidifier"; + + protected readonly _serviceAction = "set_mode"; + static getStubConfig(): HumidifierModesCardFeatureConfig { return { type: "humidifier-modes", @@ -77,121 +61,12 @@ class HuiHumidifierModesCardFeature return document.createElement("hui-humidifier-modes-card-feature-editor"); } - public setConfig(config: HumidifierModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } - } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const mode = ev.detail.value ?? ev.detail.item?.value; - - const oldMode = this._stateObj!.attributes.mode; - - if (mode === oldMode || !mode) { - return; - } - - this._currentMode = mode; - - try { - await this._setMode(mode); - } catch (_err) { - this._currentMode = oldMode; - } - } - - private async _setMode(mode: string) { - await this.hass!.callService("humidifier", "set_mode", { - entity_id: this._stateObj!.entity_id, - mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsHumidifierModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const stateObj = this._stateObj; - - const options = filterModes( - stateObj.attributes.available_modes, - this._config!.modes - ).map((mode) => ({ - value: mode, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "mode", - mode - ), - })); - - if (this._config.style === "icons") { - return html` - ({ - ...option, - icon: html``, - }))} - .value=${this._currentMode} - @value-changed=${this._valueChanged} - hide-option-label - .label=${this.hass!.formatEntityAttributeName(stateObj, "mode")} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; - } - - return html` - - - - `; - } - - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsHumidifierModesCardFeature(this.hass, this.context) + ); } } diff --git a/src/panels/lovelace/card-features/hui-media-player-sound-mode-card-feature.ts b/src/panels/lovelace/card-features/hui-media-player-sound-mode-card-feature.ts index 1020658796..091324ff61 100644 --- a/src/panels/lovelace/card-features/hui-media-player-sound-mode-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-media-player-sound-mode-card-feature.ts @@ -1,12 +1,7 @@ import type { PropertyValues } from "lit"; -import { html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; -import { UNAVAILABLE } from "../../../data/entity/entity"; import { MediaPlayerEntityFeature, type MediaPlayerEntity, @@ -14,7 +9,7 @@ import { import type { HomeAssistant } from "../../../types"; import { hasConfigChanged } from "../common/has-changed"; import type { LovelaceCardFeature } from "../types"; -import { cardFeatureStyles } from "./common/card-feature-styles"; +import { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { LovelaceCardFeatureContext, MediaPlayerSoundModeCardFeatureConfig, @@ -38,59 +33,42 @@ export const supportsMediaPlayerSoundModeCardFeature = ( @customElement("hui-media-player-sound-mode-card-feature") class HuiMediaPlayerSoundModeCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + MediaPlayerEntity, + MediaPlayerSoundModeCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "sound_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "sound_mode_list"; - @state() private _config?: MediaPlayerSoundModeCardFeatureConfig; + protected readonly _serviceDomain = "media_player"; - @state() private _currentSoundMode?: string; + protected readonly _serviceAction = "select_sound_mode"; - 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; + protected get _label(): string { + return this.hass!.localize("ui.card.media_player.sound_mode"); } + protected readonly _hideLabel = false; + + protected readonly _showDropdownOptionIcons = false; + + protected readonly _allowIconsStyle = false; + static getStubConfig(): MediaPlayerSoundModeCardFeatureConfig { return { type: "media-player-sound-mode", }; } - public setConfig(config: MediaPlayerSoundModeCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProps: PropertyValues): void { - super.willUpdate(changedProps); - if ( - (changedProps.has("hass") || changedProps.has("context")) && - this._stateObj - ) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - const oldStateObj = oldHass?.states[this.context!.entity_id!]; - if (oldStateObj !== this._stateObj) { - this._currentSoundMode = this._stateObj.attributes.sound_mode; - } - } - } - protected shouldUpdate(changedProps: PropertyValues): boolean { const entityId = this.context?.entity_id; const oldHass = changedProps.get("hass") as HomeAssistant | undefined; return ( - changedProps.has("_currentSoundMode") || + changedProps.has("_currentValue") || changedProps.has("context") || hasConfigChanged(this, changedProps) || (changedProps.has("hass") && @@ -100,64 +78,12 @@ class HuiMediaPlayerSoundModeCardFeature ); } - private async _valueChanged(ev: HaDropdownSelectEvent) { - const soundMode = ev.detail.item?.value; - const oldSoundMode = this._stateObj!.attributes.sound_mode; - - if (soundMode === oldSoundMode || !soundMode) { - return; - } - - this._currentSoundMode = soundMode; - - try { - await this.hass!.callService("media_player", "select_sound_mode", { - entity_id: this._stateObj!.entity_id, - sound_mode: soundMode, - }); - } catch (_err) { - this._currentSoundMode = oldSoundMode; - } - } - - protected render() { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsMediaPlayerSoundModeCardFeature(this.hass, this.context) - ) { - return nothing; - } - - const options = this._stateObj.attributes.sound_mode_list!.map( - (soundMode) => ({ - value: soundMode, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "sound_mode", - soundMode - ), - }) + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsMediaPlayerSoundModeCardFeature(this.hass, this.context) ); - - return html` - - - `; - } - - static get styles() { - return cardFeatureStyles; } } diff --git a/src/panels/lovelace/card-features/hui-media-player-source-card-feature.ts b/src/panels/lovelace/card-features/hui-media-player-source-card-feature.ts index ab657d4654..8dddd64d44 100644 --- a/src/panels/lovelace/card-features/hui-media-player-source-card-feature.ts +++ b/src/panels/lovelace/card-features/hui-media-player-source-card-feature.ts @@ -1,11 +1,7 @@ import type { PropertyValues } from "lit"; -import { html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { supportsFeature } from "../../../common/entity/supports-feature"; -import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; -import "../../../components/ha-control-select-menu"; -import { UNAVAILABLE } from "../../../data/entity/entity"; import { MediaPlayerEntityFeature, type MediaPlayerEntity, @@ -13,7 +9,7 @@ import { import type { HomeAssistant } from "../../../types"; import { hasConfigChanged } from "../common/has-changed"; import type { LovelaceCardFeature } from "../types"; -import { cardFeatureStyles } from "./common/card-feature-styles"; +import { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { LovelaceCardFeatureContext, MediaPlayerSourceCardFeatureConfig, @@ -37,59 +33,42 @@ export const supportsMediaPlayerSourceCardFeature = ( @customElement("hui-media-player-source-card-feature") class HuiMediaPlayerSourceCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + MediaPlayerEntity, + MediaPlayerSourceCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "source"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "source_list"; - @state() private _config?: MediaPlayerSourceCardFeatureConfig; + protected readonly _serviceDomain = "media_player"; - @state() private _currentSource?: string; + protected readonly _serviceAction = "select_source"; - 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; + protected get _label(): string { + return this.hass!.localize("ui.card.media_player.source"); } + protected readonly _hideLabel = false; + + protected readonly _showDropdownOptionIcons = false; + + protected readonly _allowIconsStyle = false; + static getStubConfig(): MediaPlayerSourceCardFeatureConfig { return { type: "media-player-source", }; } - public setConfig(config: MediaPlayerSourceCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; - } - - protected willUpdate(changedProps: PropertyValues): void { - super.willUpdate(changedProps); - if ( - (changedProps.has("hass") || changedProps.has("context")) && - this._stateObj - ) { - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - const oldStateObj = oldHass?.states[this.context!.entity_id!]; - if (oldStateObj !== this._stateObj) { - this._currentSource = this._stateObj.attributes.source; - } - } - } - protected shouldUpdate(changedProps: PropertyValues): boolean { const entityId = this.context?.entity_id; const oldHass = changedProps.get("hass") as HomeAssistant | undefined; return ( - changedProps.has("_currentSource") || + changedProps.has("_currentValue") || changedProps.has("context") || hasConfigChanged(this, changedProps) || (changedProps.has("hass") && @@ -99,61 +78,12 @@ class HuiMediaPlayerSourceCardFeature ); } - private async _valueChanged(ev: HaDropdownSelectEvent) { - const source = ev.detail.item?.value; - const oldSource = this._stateObj!.attributes.source; - - if (source === oldSource || !source) { - return; - } - - this._currentSource = source; - - try { - await this.hass!.callService("media_player", "select_source", { - entity_id: this._stateObj!.entity_id, - source: source, - }); - } catch (_err) { - this._currentSource = oldSource; - } - } - - protected render() { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsMediaPlayerSourceCardFeature(this.hass, this.context) - ) { - return nothing; - } - - const options = this._stateObj.attributes.source_list!.map((source) => ({ - value: source, - label: this.hass!.formatEntityAttributeValue( - this._stateObj!, - "source", - source - ), - })); - - return html` - - - `; - } - - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsMediaPlayerSourceCardFeature(this.hass, this.context) + ); } } diff --git a/src/panels/lovelace/card-features/hui-mode-select-card-feature-base.ts b/src/panels/lovelace/card-features/hui-mode-select-card-feature-base.ts new file mode 100644 index 0000000000..ad0ab5cb2f --- /dev/null +++ b/src/panels/lovelace/card-features/hui-mode-select-card-feature-base.ts @@ -0,0 +1,263 @@ +import type { HassEntity } from "home-assistant-js-websocket"; +import type { PropertyValues, TemplateResult } from "lit"; +import { html, LitElement, nothing } from "lit"; +import { property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import "../../../components/ha-attribute-icon"; +import "../../../components/ha-control-select"; +import "../../../components/ha-control-select-menu"; +import "../../../components/ha-svg-icon"; +import { UNAVAILABLE } from "../../../data/entity/entity"; +import type { HomeAssistant } from "../../../types"; +import type { LovelaceCardFeature } from "../types"; +import { cardFeatureStyles } from "./common/card-feature-styles"; +import { filterModes } from "./common/filter-modes"; +import type { + LovelaceCardFeatureConfig, + LovelaceCardFeatureContext, +} from "./types"; + +type AttributeModeChangeEvent = CustomEvent<{ + value?: string; + item?: { value: string }; +}>; + +type AttributeModeCardFeatureConfig = LovelaceCardFeatureConfig & { + style?: "dropdown" | "icons"; +}; + +export interface HuiModeSelectOption { + value: string; + label: string; +} + +export abstract class HuiModeSelectCardFeatureBase< + TEntity extends HassEntity, + TConfig extends AttributeModeCardFeatureConfig, +> + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + + @state() protected _config?: TConfig; + + @state() protected _currentValue?: string; + + protected abstract readonly _attribute: string; + + protected abstract readonly _modesAttribute: string; + + protected get _configuredModes(): string[] | undefined { + return undefined; + } + + protected readonly _dropdownIconPath?: string; + + protected abstract readonly _serviceDomain: string; + + protected abstract readonly _serviceAction: string; + + protected abstract _isSupported(): boolean; + + protected get _label(): string { + return this.hass!.formatEntityAttributeName( + this._stateObj!, + this._attribute + ); + } + + protected readonly _hideLabel: boolean = true; + + protected readonly _showDropdownOptionIcons: boolean = true; + + protected readonly _allowIconsStyle: boolean = true; + + protected readonly _defaultStyle: "dropdown" | "icons" = "dropdown"; + + protected get _controlSelectStyle(): + | Record + | undefined { + return undefined; + } + + protected _getServiceDomain(_stateObj: TEntity): string { + return this._serviceDomain; + } + + protected _isValueValid(_value: string, _stateObj: TEntity): boolean { + return true; + } + + protected get _stateObj(): TEntity | undefined { + if (!this.hass || !this.context?.entity_id) { + return undefined; + } + + return this.hass.states[this.context.entity_id] as TEntity | undefined; + } + + public setConfig(config: TConfig): void { + if (!config) { + throw new Error("Invalid configuration"); + } + + this._config = config; + } + + protected willUpdate(changedProps: PropertyValues): void { + super.willUpdate(changedProps); + + if ( + (changedProps.has("hass") || changedProps.has("context")) && + this._stateObj + ) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldStateObj = this.context?.entity_id + ? (oldHass?.states[this.context.entity_id] as TEntity | undefined) + : undefined; + + if (oldStateObj !== this._stateObj) { + this._currentValue = this._getValue(this._stateObj); + } + } + } + + protected render(): TemplateResult | null { + if ( + !this._config || + !this.hass || + !this.context || + !this._stateObj || + !this._isSupported() + ) { + return null; + } + + const stateObj = this._stateObj; + const options = this._getOptions(); + const label = this._label; + const renderIcons = + this._allowIconsStyle && + (this._config.style === "icons" || + (this._config.style === undefined && this._defaultStyle === "icons")); + + if (renderIcons) { + return html` + ({ + ...option, + icon: this._renderOptionIcon(option), + }))} + .value=${this._currentValue} + @value-changed=${this._valueChanged} + hide-option-label + .label=${label} + style=${styleMap(this._controlSelectStyle ?? {})} + .disabled=${stateObj.state === UNAVAILABLE} + > + + `; + } + + return html` + + ${this._dropdownIconPath + ? html`` + : nothing} + + `; + } + + protected _getValue(stateObj: TEntity): string | undefined { + return stateObj.attributes[this._attribute] as string | undefined; + } + + protected _getOptions(): HuiModeSelectOption[] { + if (!this._stateObj || !this.hass) { + return []; + } + + return filterModes( + this._stateObj.attributes[this._modesAttribute] as string[] | undefined, + this._configuredModes + ).map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this._stateObj!, + this._attribute, + mode + ), + })); + } + + protected _renderOptionIcon(option: HuiModeSelectOption): TemplateResult<1> { + return html``; + } + + private _renderMenuIcon = (value: string): TemplateResult<1> => + html``; + + private async _valueChanged(ev: AttributeModeChangeEvent) { + if (!this.hass || !this._stateObj) { + return; + } + + const value = ev.detail.value ?? ev.detail.item?.value; + const oldValue = this._getValue(this._stateObj); + + if ( + value === oldValue || + !value || + !this._isValueValid(value, this._stateObj) + ) { + return; + } + + this._currentValue = value; + + try { + await this.hass.callService( + this._getServiceDomain(this._stateObj), + this._serviceAction, + { + entity_id: this._stateObj.entity_id, + [this._attribute]: value, + } + ); + } catch (_err) { + this._currentValue = oldValue; + } + } + + static get styles() { + return cardFeatureStyles; + } +} 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 1180dd9a8c..51d2b792ef 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,22 +1,20 @@ -import type { PropertyValues } from "lit"; -import { html, LitElement, nothing } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import memoizeOne from "memoize-one"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; -import { UNAVAILABLE } from "../../../data/entity/entity"; import type { InputSelectEntity } from "../../../data/input_select"; import type { SelectEntity } from "../../../data/select"; 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 { + HuiModeSelectCardFeatureBase, + type HuiModeSelectOption, +} from "./hui-mode-select-card-feature-base"; import type { LovelaceCardFeatureContext, SelectOptionsCardFeatureConfig, } from "./types"; -import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown"; + +type SelectOptionEntity = SelectEntity | InputSelectEntity; export const supportsSelectOptionsCardFeature = ( hass: HomeAssistant, @@ -32,27 +30,32 @@ export const supportsSelectOptionsCardFeature = ( @customElement("hui-select-options-card-feature") class HuiSelectOptionsCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + SelectOptionEntity, + SelectOptionsCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "option"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "options"; - @state() private _config?: SelectOptionsCardFeatureConfig; - - @state() _currentOption?: string; - - 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; + protected get _configuredModes() { + return this._config?.options; } + protected readonly _serviceDomain = "select"; + + protected readonly _serviceAction = "select_option"; + + protected get _label(): string { + return this.hass!.localize("ui.card.select.option"); + } + + protected readonly _allowIconsStyle = false; + + protected readonly _showDropdownOptionIcons = false; + static getStubConfig(): SelectOptionsCardFeatureConfig { return { type: "select-options", @@ -64,98 +67,41 @@ class HuiSelectOptionsCardFeature return document.createElement("hui-select-options-card-feature-editor"); } - public setConfig(config: SelectOptionsCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; + protected _getValue(stateObj: SelectOptionEntity): string | undefined { + return stateObj.state; } - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } - } - } - - private async _valueChanged(ev: HaDropdownSelectEvent) { - const option = ev.detail.item?.value; - - const oldOption = this._stateObj!.state; - - if ( - option === oldOption || - !this._stateObj!.attributes.options.includes(option) - ) { - return; + protected _getOptions(): HuiModeSelectOption[] { + if (!this._stateObj || !this.hass) { + return []; } - this._currentOption = option; - - try { - await this._setOption(option); - } catch (_err) { - this._currentOption = oldOption; - } - } - - private async _setOption(option: string) { - const domain = computeDomain(this._stateObj!.entity_id); - await this.hass!.callService(domain, "select_option", { - entity_id: this._stateObj!.entity_id, - option: option, - }); - } - - protected render() { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsSelectOptionsCardFeature(this.hass, this.context) - ) { - return nothing; - } - - const stateObj = this._stateObj; - - const options = this._getOptions( + return filterModes( this._stateObj.attributes.options, - this._config.options - ); - - return html` - - - `; + this._config?.options + ).map((option) => ({ + value: option, + label: this.hass!.formatEntityState(this._stateObj!, option), + })); } - private _getOptions = memoizeOne( - (attributeOptions: string[], configOptions: string[] | undefined) => - filterModes(attributeOptions, configOptions).map((option) => ({ - value: option, - label: this.hass!.formatEntityState(this._stateObj!, option), - })) - ); + protected _getServiceDomain(stateObj: SelectOptionEntity): string { + return computeDomain(stateObj.entity_id); + } - static get styles() { - return cardFeatureStyles; + protected _isValueValid( + value: string, + stateObj: SelectOptionEntity + ): boolean { + return stateObj.attributes.options.includes(value); + } + + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsSelectOptionsCardFeature(this.hass, this.context) + ); } } 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 7b007ab28e..1c9d1d6929 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,24 +1,13 @@ import { mdiWaterBoiler } from "@mdi/js"; -import type { PropertyValues, TemplateResult } from "lit"; -import { html, LitElement } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; +import { customElement } from "lit/decorators"; import { computeDomain } from "../../../common/entity/compute_domain"; import { stateColorCss } from "../../../common/entity/state_color"; -import "../../../components/ha-attribute-icon"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-select-menu"; -import "../../../components/ha-list-item"; -import { UNAVAILABLE } from "../../../data/entity/entity"; -import type { - OperationMode, - WaterHeaterEntity, -} from "../../../data/water_heater"; +import type { WaterHeaterEntity } from "../../../data/water_heater"; import { compareWaterHeaterOperationMode } from "../../../data/water_heater"; 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 { HuiModeSelectCardFeatureBase } from "./hui-mode-select-card-feature-base"; import type { LovelaceCardFeatureContext, WaterHeaterOperationModesCardFeatureConfig, @@ -38,32 +27,42 @@ export const supportsWaterHeaterOperationModesCardFeature = ( @customElement("hui-water-heater-operation-modes-card-feature") class HuiWaterHeaterOperationModeCardFeature - extends LitElement + extends HuiModeSelectCardFeatureBase< + WaterHeaterEntity, + WaterHeaterOperationModesCardFeatureConfig + > implements LovelaceCardFeature { - @property({ attribute: false }) public hass?: HomeAssistant; + protected readonly _attribute = "operation_mode"; - @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + protected readonly _modesAttribute = "operation_list"; - @state() private _config?: WaterHeaterOperationModesCardFeatureConfig; + protected get _configuredModes() { + return this._config?.operation_modes; + } - @state() _currentOperationMode?: OperationMode; + protected readonly _dropdownIconPath = mdiWaterBoiler; - private _renderOperationModeIcon = (value: string) => - html``; + protected readonly _serviceDomain = "water_heater"; - private get _stateObj() { - if (!this.hass || !this.context || !this.context.entity_id) { + protected readonly _serviceAction = "set_operation_mode"; + + protected get _label(): string { + return this.hass!.localize("ui.card.water_heater.mode"); + } + + protected readonly _defaultStyle = "icons"; + + protected get _controlSelectStyle(): + | Record + | undefined { + if (!this._stateObj) { return undefined; } - return this.hass.states[this.context.entity_id!] as - | WaterHeaterEntity - | undefined; + + return { + "--control-select-color": stateColorCss(this._stateObj), + }; } static getStubConfig(): WaterHeaterOperationModesCardFeatureConfig { @@ -79,125 +78,34 @@ class HuiWaterHeaterOperationModeCardFeature ); } - public setConfig(config: WaterHeaterOperationModesCardFeatureConfig): void { - if (!config) { - throw new Error("Invalid configuration"); - } - this._config = config; + protected _getValue(stateObj: WaterHeaterEntity): string | undefined { + return stateObj.state; } - protected willUpdate(changedProp: PropertyValues): void { - super.willUpdate(changedProp); - 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; - } + protected _getOptions() { + if (!this._stateObj || !this.hass) { + return []; } - } - - private async _valueChanged( - ev: CustomEvent<{ value?: string; item?: { value: string } }> - ) { - const mode = ev.detail.value ?? ev.detail.item?.value; - - if (mode === this._stateObj!.state || !mode) { - return; - } - - const oldMode = this._stateObj!.state as OperationMode; - this._currentOperationMode = mode as OperationMode; - - try { - await this._setMode(this._currentOperationMode); - } catch (_err) { - this._currentOperationMode = oldMode; - } - } - - private async _setMode(mode: OperationMode) { - await this.hass!.callService("water_heater", "set_operation_mode", { - entity_id: this._stateObj!.entity_id, - operation_mode: mode, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.context || - !this._stateObj || - !supportsWaterHeaterOperationModesCardFeature(this.hass, this.context) - ) { - return null; - } - - const color = stateColorCss(this._stateObj); const orderedModes = (this._stateObj.attributes.operation_list || []) .concat() .sort(compareWaterHeaterOperationMode) .reverse(); - const options = filterModes(orderedModes, this._config.operation_modes).map( + return filterModes(orderedModes, this._config?.operation_modes).map( (mode) => ({ value: mode, label: this.hass!.formatEntityState(this._stateObj!, mode), }) ); - - if (this._config.style === "dropdown") { - return html` - - - - `; - } - - return html` - ({ - ...option, - icon: html` - - `, - }))} - .value=${this._currentOperationMode} - @value-changed=${this._valueChanged} - hide-option-label - .label=${this.hass.localize("ui.card.water_heater.mode")} - style=${styleMap({ - "--control-select-color": color, - })} - .disabled=${this._stateObj!.state === UNAVAILABLE} - > - - `; } - static get styles() { - return cardFeatureStyles; + protected _isSupported(): boolean { + return !!( + this.hass && + this.context && + supportsWaterHeaterOperationModesCardFeature(this.hass, this.context) + ); } }