From a1b039c8d760d72206b47ac484be3ede41ad6c9d Mon Sep 17 00:00:00 2001 From: Muka Schultze Date: Tue, 5 Mar 2024 23:32:02 +0000 Subject: [PATCH] feat: add swing modes card feature --- gallery/src/pages/lovelace/thermostat-card.ts | 12 +- .../hui-climate-swing-modes-card-feature.ts | 232 ++++++++++++++++++ src/panels/lovelace/card-features/types.ts | 7 + .../create-card-feature-element.ts | 2 + .../hui-card-features-editor.ts | 4 + ...climate-swing-modes-card-feature-editor.ts | 133 ++++++++++ .../hui-thermostat-card-editor.ts | 1 + src/translations/en.json | 9 + 8 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts create mode 100644 src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts diff --git a/gallery/src/pages/lovelace/thermostat-card.ts b/gallery/src/pages/lovelace/thermostat-card.ts index 1775a826e2b9..da1300782850 100644 --- a/gallery/src/pages/lovelace/thermostat-card.ts +++ b/gallery/src/pages/lovelace/thermostat-card.ts @@ -46,7 +46,9 @@ const ENTITIES = [ friendly_name: "Sensibo purifier", fan_modes: ["low", "high"], fan_mode: "low", - supported_features: 9, + swing_modes: ["on", "off", "both", "vertical", "horizontal"], + swing_mode: "vertical", + supported_features: 41, }), getEntity("climate", "unavailable", "unavailable", { supported_features: 43, @@ -85,6 +87,14 @@ const CONFIGS = [ fan_modes: - low - high + - type: climate-swing-modes + style: icons + swing_modes: + - 'on' + - 'off' + - 'both' + - 'vertical' + - 'horizontal' `, }, { 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 new file mode 100644 index 000000000000..ec7dadaa8496 --- /dev/null +++ b/src/panels/lovelace/card-features/hui-climate-swing-modes-card-feature.ts @@ -0,0 +1,232 @@ +import { mdiArrowOscillating } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; +import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; +import { stopPropagation } from "../../../common/dom/stop_propagation"; +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 type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; +import { ClimateEntity, ClimateEntityFeature } from "../../../data/climate"; +import { UNAVAILABLE } from "../../../data/entity"; +import { HomeAssistant } from "../../../types"; +import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; +import { ClimateSwingModesCardFeatureConfig } from "./types"; + +export const supportsClimateSwingModesCardFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return ( + domain === "climate" && + supportsFeature(stateObj, ClimateEntityFeature.SWING_MODE) + ); +}; + +@customElement("hui-climate-swing-modes-card-feature") +class HuiClimateSwingModesCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: ClimateEntity; + + @state() private _config?: ClimateSwingModesCardFeatureConfig; + + @state() _currentSwingMode?: string; + + @query("ha-control-select-menu", true) + private _haSelect?: HaControlSelectMenu; + + static getStubConfig( + _, + stateObj?: HassEntity + ): ClimateSwingModesCardFeatureConfig { + return { + type: "climate-swing-modes", + style: "dropdown", + swing_modes: stateObj?.attributes.swing_modes || [], + }; + } + + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-climate-swing-modes-card-feature-editor" + ); + return document.createElement( + "hui-climate-swing-modes-card-feature-editor" + ); + } + + 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("stateObj") && this.stateObj) { + this._currentSwingMode = this.stateObj.attributes.swing_mode; + } + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (this._haSelect && changedProps.has("hass")) { + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + if ( + this.hass && + this.hass.formatEntityAttributeValue !== + oldHass?.formatEntityAttributeValue + ) { + this._haSelect.layoutOptions(); + } + } + } + + private async _valueChanged(ev: CustomEvent) { + const swingMode = + (ev.detail as any).value ?? ((ev.target as any).value as string); + + const oldSwingMode = this.stateObj!.attributes.swing_mode; + + if (swingMode === oldSwingMode) 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.stateObj || + !supportsClimateSwingModesCardFeature(this.stateObj) + ) { + return null; + } + + const stateObj = this.stateObj; + + const modes = stateObj.attributes.swing_modes || []; + + const options = modes + .filter((mode) => (this._config!.swing_modes || []).includes(mode)) + .map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "swing_mode", + mode + ), + icon: html``, + })); + + if (this._config.style === "icons") { + return html` +
+ + +
+ `; + } + + return html` +
+ + ${this._currentSwingMode + ? html`` + : html` `} + ${options.map( + (option) => html` + + ${option.icon}${option.label} + + ` + )} + +
+ `; + } + + static get styles() { + return css` + ha-control-select-menu { + box-sizing: border-box; + --control-select-menu-height: 40px; + --control-select-menu-border-radius: 10px; + line-height: 1.2; + display: block; + width: 100%; + } + ha-control-select { + --control-select-color: var(--feature-color); + --control-select-padding: 0; + --control-select-thickness: 40px; + --control-select-border-radius: 10px; + --control-select-button-border-radius: 10px; + } + .container { + padding: 0 12px 12px 12px; + width: auto; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-climate-swing-modes-card-feature": HuiClimateSwingModesCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index f96427b4ba52..7b3771a2d86f 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -47,6 +47,12 @@ export interface ClimateFanModesCardFeatureConfig { fan_modes?: string[]; } +export interface ClimateSwingModesCardFeatureConfig { + type: "climate-swing-modes"; + style?: "dropdown" | "icons"; + swing_modes?: string[]; +} + export interface ClimateHvacModesCardFeatureConfig { type: "climate-hvac-modes"; style?: "dropdown" | "icons"; @@ -123,6 +129,7 @@ export interface UpdateActionsCardFeatureConfig { export type LovelaceCardFeatureConfig = | AlarmModesCardFeatureConfig | ClimateFanModesCardFeatureConfig + | ClimateSwingModesCardFeatureConfig | ClimateHvacModesCardFeatureConfig | ClimatePresetModesCardFeatureConfig | CoverOpenCloseCardFeatureConfig diff --git a/src/panels/lovelace/create-element/create-card-feature-element.ts b/src/panels/lovelace/create-element/create-card-feature-element.ts index 8aec1360e382..4054f4610ecc 100644 --- a/src/panels/lovelace/create-element/create-card-feature-element.ts +++ b/src/panels/lovelace/create-element/create-card-feature-element.ts @@ -1,5 +1,6 @@ import "../card-features/hui-alarm-modes-card-feature"; import "../card-features/hui-climate-fan-modes-card-feature"; +import "../card-features/hui-climate-swing-modes-card-feature"; import "../card-features/hui-climate-hvac-modes-card-feature"; import "../card-features/hui-climate-preset-modes-card-feature"; import "../card-features/hui-cover-open-close-card-feature"; @@ -30,6 +31,7 @@ import { const TYPES: Set = new Set([ "alarm-modes", "climate-fan-modes", + "climate-swing-modes", "climate-hvac-modes", "climate-preset-modes", "cover-open-close", 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 ef1fcdfae0bd..444144925fa9 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 @@ -21,6 +21,7 @@ import { import { HomeAssistant } from "../../../../types"; import { supportsAlarmModesCardFeature } from "../../card-features/hui-alarm-modes-card-feature"; import { supportsClimateFanModesCardFeature } from "../../card-features/hui-climate-fan-modes-card-feature"; +import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-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 { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature"; @@ -50,6 +51,7 @@ type SupportsFeature = (stateObj: HassEntity) => boolean; const UI_FEATURE_TYPES = [ "alarm-modes", "climate-fan-modes", + "climate-swing-modes", "climate-hvac-modes", "climate-preset-modes", "cover-open-close", @@ -78,6 +80,7 @@ const EDITABLES_FEATURE_TYPES = new Set([ "alarm-modes", "climate-hvac-modes", "climate-fan-modes", + "climate-swing-modes", "climate-preset-modes", "fan-preset-modes", "humidifier-modes", @@ -94,6 +97,7 @@ const SUPPORTS_FEATURE_TYPES: Record< > = { "alarm-modes": supportsAlarmModesCardFeature, "climate-fan-modes": supportsClimateFanModesCardFeature, + "climate-swing-modes": supportsClimateSwingModesCardFeature, "climate-hvac-modes": supportsClimateHvacModesCardFeature, "climate-preset-modes": supportsClimatePresetModesCardFeature, "cover-open-close": supportsCoverOpenCloseCardFeature, diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts new file mode 100644 index 000000000000..812c5c03cba6 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-climate-swing-modes-card-feature-editor.ts @@ -0,0 +1,133 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import { FormatEntityAttributeValueFunc } from "../../../../common/translations/entity-state"; +import { LocalizeFunc } from "../../../../common/translations/localize"; +import "../../../../components/ha-form/ha-form"; +import type { + HaFormSchema, + SchemaUnion, +} from "../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../types"; +import { + ClimateSwingModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "../../card-features/types"; +import type { LovelaceCardFeatureEditor } from "../../types"; + +@customElement("hui-climate-swing-modes-card-feature-editor") +export class HuiClimateSwingModesCardFeatureEditor + extends LitElement + implements LovelaceCardFeatureEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + + @state() private _config?: ClimateSwingModesCardFeatureConfig; + + public setConfig(config: ClimateSwingModesCardFeatureConfig): void { + this._config = config; + } + + private _schema = memoizeOne( + ( + localize: LocalizeFunc, + formatEntityAttributeValue: FormatEntityAttributeValueFunc, + stateObj?: HassEntity + ) => + [ + { + name: "style", + selector: { + select: { + multiple: false, + mode: "list", + options: ["dropdown", "icons"].map((mode) => ({ + value: mode, + label: localize( + `ui.panel.lovelace.editor.features.types.climate-swing-modes.style_list.${mode}` + ), + })), + }, + }, + }, + { + name: "swing_modes", + selector: { + select: { + multiple: true, + mode: "list", + options: + stateObj?.attributes.swing_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "swing_mode", + mode + ), + })) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[] + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + const stateObj = this.context?.entity_id + ? this.hass.states[this.context?.entity_id] + : undefined; + + const data: ClimateSwingModesCardFeatureConfig = { + style: "dropdown", + swing_modes: [], + ...this._config, + }; + + const schema = this._schema( + this.hass.localize, + this.hass.formatEntityAttributeValue, + stateObj + ); + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "style": + case "swing_modes": + return this.hass!.localize( + `ui.panel.lovelace.editor.features.types.climate-swing-modes.${schema.name}` + ); + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-climate-swing-modes-card-feature-editor": HuiClimateSwingModesCardFeatureEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts index a126496d0aa8..4d38affa8e1d 100644 --- a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts @@ -34,6 +34,7 @@ const COMPATIBLE_FEATURES_TYPES: FeatureType[] = [ "climate-hvac-modes", "climate-preset-modes", "climate-fan-modes", + "climate-swing-modes", ]; const cardConfigStruct = assign( diff --git a/src/translations/en.json b/src/translations/en.json index 039e0d91765c..41d95f039178 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5674,6 +5674,15 @@ }, "fan_modes": "Fan modes" }, + "climate-swing-modes": { + "label": "Climate swing modes", + "style": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style%]", + "style_list": { + "dropdown": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::dropdown%]", + "icons": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::style_list::icons%]" + }, + "swing_modes": "Swing modes" + }, "climate-hvac-modes": { "label": "Climate HVAC modes", "hvac_modes": "HVAC modes",