From 988fa3e4e4c89da5fd2537ae9faeea090bd49553 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Wed, 27 Nov 2024 15:10:11 +0100 Subject: [PATCH] Add horizontal swing to climate (#22043) --- gallery/src/pages/lovelace/thermostat-card.ts | 18 +- gallery/src/pages/lovelace/tile-card.ts | 3 + src/data/climate.ts | 3 + .../more-info/controls/more-info-climate.ts | 67 ++++++ ...ate-swing-horizontal-modes-card-feature.ts | 214 ++++++++++++++++++ src/panels/lovelace/card-features/types.ts | 7 + .../create-card-feature-element.ts | 1 + .../hui-card-features-editor.ts | 5 + ...ng-horizontal-modes-card-feature-editor.ts | 168 ++++++++++++++ .../hui-thermostat-card-editor.ts | 1 + src/translations/en.json | 10 + 11 files changed, 490 insertions(+), 7 deletions(-) create mode 100644 src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts create mode 100644 src/panels/lovelace/editor/config-elements/hui-climate-swing-horizontal-modes-card-feature-editor.ts diff --git a/gallery/src/pages/lovelace/thermostat-card.ts b/gallery/src/pages/lovelace/thermostat-card.ts index b8d40d25e5..dc0a572e59 100644 --- a/gallery/src/pages/lovelace/thermostat-card.ts +++ b/gallery/src/pages/lovelace/thermostat-card.ts @@ -86,9 +86,11 @@ const ENTITIES = [ friendly_name: "Sensibo purifier", fan_modes: ["low", "high"], fan_mode: "low", - swing_modes: ["on", "off", "both", "vertical", "horizontal"], - swing_mode: "vertical", - supported_features: 41, + swing_modes: ["both", "rangefull", "off"], + swing_mode: "rangefull", + swing_horizontal_modes: ["both", "rangefull", "off"], + swing_horizontal_mode: "both", + supported_features: 553, }), getEntity("climate", "unavailable", "unavailable", { supported_features: 43, @@ -188,11 +190,13 @@ const CONFIGS = [ - type: climate-swing-modes style: icons swing_modes: - - 'on' - - 'off' - 'both' - - 'vertical' - - 'horizontal' + - 'rangefull' + - 'off' + swing_horizontal_modes: + - 'both' + - 'rangefull' + - 'off' `, }, { diff --git a/gallery/src/pages/lovelace/tile-card.ts b/gallery/src/pages/lovelace/tile-card.ts index 59ba3a150e..c5289165ea 100644 --- a/gallery/src/pages/lovelace/tile-card.ts +++ b/gallery/src/pages/lovelace/tile-card.ts @@ -78,16 +78,19 @@ const ENTITIES = [ fan_modes: ["on_low", "on_high", "auto_low", "auto_high", "off"], preset_modes: ["home", "eco", "away"], swing_modes: ["auto", "1", "2", "3", "off"], + switch_horizontal_modes: ["auto", "4", "5", "6", "off"], current_temperature: 23, target_temp_high: 24, target_temp_low: 21, fan_mode: "auto_low", preset_mode: "home", swing_mode: "auto", + swing_horizontal_mode: "off", supported_features: ClimateEntityFeature.TURN_ON + ClimateEntityFeature.TURN_OFF + ClimateEntityFeature.SWING_MODE + + ClimateEntityFeature.SWING_HORIZONTAL_MODE + ClimateEntityFeature.PRESET_MODE + ClimateEntityFeature.FAN_MODE + ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, diff --git a/src/data/climate.ts b/src/data/climate.ts index b6e3f3ab6d..8e59567a6a 100644 --- a/src/data/climate.ts +++ b/src/data/climate.ts @@ -61,6 +61,8 @@ export type ClimateEntity = HassEntityBase & { preset_modes?: string[]; swing_mode?: string; swing_modes?: string[]; + swing_horizontal_mode?: string; + swing_horizontal_modes?: string[]; aux_heat?: "on" | "off"; }; }; @@ -75,6 +77,7 @@ export const enum ClimateEntityFeature { AUX_HEAT = 64, TURN_OFF = 128, TURN_ON = 256, + SWING_HORIZONTAL_MODE = 512, } const hvacModeOrdering = HVAC_MODES.reduce( diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 450092c4ca..28f1c24554 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -74,6 +74,10 @@ class MoreInfoClimate extends LitElement { stateObj, ClimateEntityFeature.SWING_MODE ); + const supportSwingHorizontalMode = supportsFeature( + stateObj, + ClimateEntityFeature.SWING_HORIZONTAL_MODE + ); const currentTemperature = this.stateObj.attributes.current_temperature; const currentHumidity = this.stateObj.attributes.current_humidity; @@ -344,6 +348,59 @@ class MoreInfoClimate extends LitElement { ` : nothing} + ${supportSwingHorizontalMode && + stateObj.attributes.swing_horizontal_modes + ? html` + + ${stateObj.attributes.swing_horizontal_mode + ? html` + + ` + : html` + + `} + ${stateObj.attributes.swing_horizontal_modes!.map( + (mode) => html` + + + ${this.hass.formatEntityAttributeValue( + stateObj, + "swing_horizontal_mode", + mode + )} + + ` + )} + + ` + : nothing} `; } @@ -380,6 +437,16 @@ class MoreInfoClimate extends LitElement { ); } + private _handleSwingHorizontalmodeChanged(ev) { + const newVal = ev.target.value; + this._callServiceHelper( + this.stateObj!.attributes.swing_horizontal_mode, + newVal, + "set_swing_horizontal_mode", + { swing_horizontal_mode: newVal } + ); + } + private _handlePresetmodeChanged(ev) { const newVal = ev.target.value || null; if (newVal) { 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 new file mode 100644 index 0000000000..fdf242434e --- /dev/null +++ b/src/panels/lovelace/card-features/hui-climate-swing-horizontal-modes-card-feature.ts @@ -0,0 +1,214 @@ +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"; +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 type { ClimateEntity } from "../../../data/climate"; +import { ClimateEntityFeature } from "../../../data/climate"; +import { UNAVAILABLE } from "../../../data/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 type { ClimateSwingHorizontalModesCardFeatureConfig } from "./types"; + +export const supportsClimateSwingHorizontalModesCardFeature = ( + stateObj: HassEntity +) => { + const domain = computeDomain(stateObj.entity_id); + return ( + domain === "climate" && + supportsFeature(stateObj, ClimateEntityFeature.SWING_HORIZONTAL_MODE) + ); +}; + +@customElement("hui-climate-swing-horizontal-modes-card-feature") +class HuiClimateSwingHorizontalModesCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: ClimateEntity; + + @state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig; + + @state() _currentSwingHorizontalMode?: string; + + @query("ha-control-select-menu", true) + private _haSelect?: HaControlSelectMenu; + + static getStubConfig(): ClimateSwingHorizontalModesCardFeatureConfig { + return { + type: "climate-swing-horizontal-modes", + style: "dropdown", + }; + } + + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-climate-swing-horizontal-modes-card-feature-editor" + ); + return document.createElement( + "hui-climate-swing-horizontal-modes-card-feature-editor" + ); + } + + 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("stateObj") && this.stateObj) { + this._currentSwingHorizontalMode = + this.stateObj.attributes.swing_horizontal_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 swingHorizontalMode = + (ev.detail as any).value ?? ((ev.target as any).value as string); + + const oldSwingHorizontalMode = + this.stateObj!.attributes.swing_horizontal_mode; + + if (swingHorizontalMode === oldSwingHorizontalMode) 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.stateObj || + !supportsClimateSwingHorizontalModesCardFeature(this.stateObj) + ) { + 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 + ), + icon: html``, + })); + + if (this._config.style === "icons") { + return html` + + + `; + } + + return html` + + ${this._currentSwingHorizontalMode + ? html`` + : html` `} + ${options.map( + (option) => html` + + ${option.icon}${option.label} + + ` + )} + + `; + } + + static get styles() { + return cardFeatureStyles; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-climate-swing-horizontal-modes-card-feature": HuiClimateSwingHorizontalModesCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index 4d965bdde3..ec60914086 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -61,6 +61,12 @@ export interface ClimateSwingModesCardFeatureConfig { swing_modes?: string[]; } +export interface ClimateSwingHorizontalModesCardFeatureConfig { + type: "climate-swing-horizontal-modes"; + style?: "dropdown" | "icons"; + swing_horizontal_modes?: string[]; +} + export interface ClimateHvacModesCardFeatureConfig { type: "climate-hvac-modes"; style?: "dropdown" | "icons"; @@ -139,6 +145,7 @@ export type LovelaceCardFeatureConfig = | AlarmModesCardFeatureConfig | ClimateFanModesCardFeatureConfig | ClimateSwingModesCardFeatureConfig + | ClimateSwingHorizontalModesCardFeatureConfig | 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 8aa564e7f0..04f0efa876 100644 --- a/src/panels/lovelace/create-element/create-card-feature-element.ts +++ b/src/panels/lovelace/create-element/create-card-feature-element.ts @@ -34,6 +34,7 @@ const TYPES: Set = new Set([ "alarm-modes", "climate-fan-modes", "climate-swing-modes", + "climate-swing-horizontal-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 e89417ab36..41564ebe5e 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 @@ -24,6 +24,7 @@ import { supportsClimateFanModesCardFeature } from "../../card-features/hui-clim 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 { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature"; import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature"; import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature"; @@ -56,6 +57,7 @@ const UI_FEATURE_TYPES = [ "climate-hvac-modes", "climate-preset-modes", "climate-swing-modes", + "climate-swing-horizontal-modes", "cover-open-close", "cover-position", "cover-tilt-position", @@ -86,6 +88,7 @@ const EDITABLES_FEATURE_TYPES = new Set([ "climate-hvac-modes", "climate-preset-modes", "climate-swing-modes", + "climate-swing-horizontal-modes", "fan-preset-modes", "humidifier-modes", "lawn-mower-commands", @@ -103,6 +106,8 @@ const SUPPORTS_FEATURE_TYPES: Record< "alarm-modes": supportsAlarmModesCardFeature, "climate-fan-modes": supportsClimateFanModesCardFeature, "climate-swing-modes": supportsClimateSwingModesCardFeature, + "climate-swing-horizontal-modes": + supportsClimateSwingHorizontalModesCardFeature, "climate-hvac-modes": supportsClimateHvacModesCardFeature, "climate-preset-modes": supportsClimatePresetModesCardFeature, "cover-open-close": supportsCoverOpenCloseCardFeature, diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-swing-horizontal-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-swing-horizontal-modes-card-feature-editor.ts new file mode 100644 index 0000000000..0e7da95fe5 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-climate-swing-horizontal-modes-card-feature-editor.ts @@ -0,0 +1,168 @@ +import type { 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 type { FormatEntityAttributeValueFunc } from "../../../../common/translations/entity-state"; +import type { 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 type { + ClimateSwingHorizontalModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "../../card-features/types"; +import type { LovelaceCardFeatureEditor } from "../../types"; + +type ClimateSwingHorizontalModesCardFeatureData = + ClimateSwingHorizontalModesCardFeatureConfig & { + customize_modes: boolean; + }; + +@customElement("hui-climate-swing-horizontal-modes-card-feature-editor") +export class HuiClimateSwingHorizontalModesCardFeatureEditor + extends LitElement + implements LovelaceCardFeatureEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + + @state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig; + + public setConfig(config: ClimateSwingHorizontalModesCardFeatureConfig): void { + this._config = config; + } + + private _schema = memoizeOne( + ( + localize: LocalizeFunc, + formatEntityAttributeValue: FormatEntityAttributeValueFunc, + stateObj: HassEntity | undefined, + customizeModes: boolean + ) => + [ + { + 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-horizontal-modes.style_list.${mode}` + ), + })), + }, + }, + }, + { + name: "customize_modes", + selector: { + boolean: {}, + }, + }, + ...(customizeModes + ? ([ + { + name: "swing_horizontal_modes", + selector: { + select: { + reorder: true, + multiple: true, + options: + stateObj?.attributes.swing_horizontal_modes?.map( + (mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "swing_horizontal_mode", + mode + ), + }) + ) || [], + }, + }, + }, + ] as const satisfies readonly HaFormSchema[]) + : []), + ] 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: ClimateSwingHorizontalModesCardFeatureData = { + style: "dropdown", + ...this._config, + customize_modes: this._config.swing_horizontal_modes !== undefined, + }; + + const schema = this._schema( + this.hass.localize, + this.hass.formatEntityAttributeValue, + stateObj, + data.customize_modes + ); + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + const { customize_modes, ...config } = ev.detail + .value as ClimateSwingHorizontalModesCardFeatureData; + + const stateObj = this.context?.entity_id + ? this.hass!.states[this.context?.entity_id] + : undefined; + + if (customize_modes && !config.swing_horizontal_modes) { + config.swing_horizontal_modes = + stateObj?.attributes.swing_horizontal_modes || []; + } + if (!customize_modes && config.swing_horizontal_modes) { + delete config.swing_horizontal_modes; + } + + fireEvent(this, "config-changed", { config: config }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "style": + case "swing_horizontal_modes": + case "customize_modes": + return this.hass!.localize( + `ui.panel.lovelace.editor.features.types.climate-swing-horizontal-modes.${schema.name}` + ); + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-climate-swing-horizontal-modes-card-feature-editor": HuiClimateSwingHorizontalModesCardFeatureEditor; + } +} 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 177f335e90..93977539ef 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 @@ -38,6 +38,7 @@ const COMPATIBLE_FEATURES_TYPES: FeatureType[] = [ "climate-preset-modes", "climate-fan-modes", "climate-swing-modes", + "climate-swing-horizontal-modes", ]; const cardConfigStruct = assign( diff --git a/src/translations/en.json b/src/translations/en.json index c49ffd2ed1..221ad621bc 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -6541,6 +6541,16 @@ }, "customize_modes": "Customize swing modes" }, + "climate-swing-horizontal-modes": { + "label": "Climate horizontal swing modes", + "swing_horizontal_modes": "Horizontal 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%]" + }, + "customize_modes": "Customize horizontal swing modes" + }, "climate-hvac-modes": { "label": "Climate HVAC modes", "hvac_modes": "HVAC modes",