From 7e55b9e6b8addea4854d7847b65bb6f11dd2ffaf Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 5 Dec 2023 18:06:03 +0100 Subject: [PATCH] Add humidifier modes and toggle feature (#18912) --- .../hui-humidifier-modes-card-feature.ts | 164 +++++++++++++----- .../hui-humidifier-toggle-card-feature.ts | 136 +++++++++++++++ src/panels/lovelace/card-features/types.ts | 7 + .../lovelace/cards/hui-humidifier-card.ts | 2 +- .../create-card-feature-element.ts | 2 + .../hui-card-features-editor.ts | 4 + .../hui-humidifier-card-editor.ts | 10 +- ...ui-humidifier-modes-card-feature-editor.ts | 129 ++++++++++++++ src/translations/en.json | 11 +- 9 files changed, 419 insertions(+), 46 deletions(-) create mode 100644 src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts create mode 100644 src/panels/lovelace/editor/config-elements/hui-humidifier-modes-card-feature-editor.ts 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 a0c7f66005..a3e4c845aa 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,25 +1,34 @@ -import { mdiPower, mdiWaterPercent } from "@mdi/js"; +import { mdiTuneVariant } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; -import { LitElement, PropertyValues, TemplateResult, css, html } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; +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 { stateColorCss } from "../../../common/entity/state_color"; +import { supportsFeature } from "../../../common/entity/supports-feature"; 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 { + HumidifierEntityFeature, + HumidifierEntity, + computeHumidiferModeIcon, +} from "../../../data/humidifier"; import { UNAVAILABLE } from "../../../data/entity"; -import { HumidifierEntity, HumidifierState } from "../../../data/humidifier"; import { HomeAssistant } from "../../../types"; -import { LovelaceCardFeature } from "../types"; +import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; import { HumidifierModesCardFeatureConfig } from "./types"; export const supportsHumidifierModesCardFeature = (stateObj: HassEntity) => { const domain = computeDomain(stateObj.entity_id); - return domain === "humidifier"; + return ( + domain === "humidifier" && + supportsFeature(stateObj, HumidifierEntityFeature.MODES) + ); }; @customElement("hui-humidifier-modes-card-feature") -class HuiHumidifierModeCardFeature +class HuiHumidifierModesCardFeature extends LitElement implements LovelaceCardFeature { @@ -29,14 +38,29 @@ class HuiHumidifierModeCardFeature @state() private _config?: HumidifierModesCardFeatureConfig; - @state() _currentState?: HumidifierState; + @state() _currentMode?: string; - static getStubConfig(): HumidifierModesCardFeatureConfig { + @query("ha-control-select-menu", true) + private _haSelect?: HaControlSelectMenu; + + static getStubConfig( + _, + stateObj?: HassEntity + ): HumidifierModesCardFeatureConfig { return { type: "humidifier-modes", + style: "dropdown", + modes: stateObj?.attributes.available_modes || [], }; } + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-humidifier-modes-card-feature-editor" + ); + return document.createElement("hui-humidifier-modes-card-feature-editor"); + } + public setConfig(config: HumidifierModesCardFeatureConfig): void { if (!config) { throw new Error("Invalid configuration"); @@ -47,33 +71,46 @@ class HuiHumidifierModeCardFeature protected willUpdate(changedProp: PropertyValues): void { super.willUpdate(changedProp); if (changedProp.has("stateObj") && this.stateObj) { - this._currentState = this.stateObj.state as HumidifierState; + this._currentMode = this.stateObj.attributes.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 newState = (ev.detail as any).value as HumidifierState; + const mode = + (ev.detail as any).value ?? ((ev.target as any).value as string); - if (newState === this.stateObj!.state) return; + const oldMode = this.stateObj!.attributes.mode; - const oldState = this.stateObj!.state as HumidifierState; - this._currentState = newState; + if (mode === oldMode) return; + + this._currentMode = mode; try { - await this._setState(newState); + await this._setMode(mode); } catch (err) { - this._currentState = oldState; + this._currentMode = oldMode; } } - private async _setState(newState: HumidifierState) { - await this.hass!.callService( - "humidifier", - newState === "on" ? "turn_on" : "turn_off", - { - entity_id: this.stateObj!.entity_id, - } - ); + private async _setMode(mode: string) { + await this.hass!.callService("humidifier", "set_mode", { + entity_id: this.stateObj!.entity_id, + mode: mode, + }); } protected render(): TemplateResult | null { @@ -86,34 +123,75 @@ class HuiHumidifierModeCardFeature return null; } - const color = stateColorCss(this.stateObj); + const stateObj = this.stateObj; - const options = ["on", "off"].map((entityState) => ({ - value: entityState, - label: this.hass!.formatEntityState(this.stateObj!, entityState), - path: entityState === "on" ? mdiWaterPercent : mdiPower, - })); + const modes = stateObj.attributes.available_modes || []; + + const options = modes + .filter((mode) => (this._config!.modes || []).includes(mode)) + .map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "mode", + mode + ), + path: computeHumidiferModeIcon(mode), + })); + + if (this._config.style === "icons") { + return html` +
+ + +
+ `; + } return html`
- - + + ${options.map( + (option) => html` + + + ${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; @@ -131,6 +209,6 @@ class HuiHumidifierModeCardFeature declare global { interface HTMLElementTagNameMap { - "hui-humidifier-modes-card-feature": HuiHumidifierModeCardFeature; + "hui-humidifier-modes-card-feature": HuiHumidifierModesCardFeature; } } diff --git a/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts b/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts new file mode 100644 index 0000000000..647da49f1a --- /dev/null +++ b/src/panels/lovelace/card-features/hui-humidifier-toggle-card-feature.ts @@ -0,0 +1,136 @@ +import { mdiPower, mdiWaterPercent } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; +import { LitElement, PropertyValues, TemplateResult, css, html } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { styleMap } from "lit/directives/style-map"; +import { computeDomain } from "../../../common/entity/compute_domain"; +import { stateColorCss } from "../../../common/entity/state_color"; +import "../../../components/ha-control-select"; +import type { ControlSelectOption } from "../../../components/ha-control-select"; +import { UNAVAILABLE } from "../../../data/entity"; +import { HumidifierEntity, HumidifierState } from "../../../data/humidifier"; +import { HomeAssistant } from "../../../types"; +import { LovelaceCardFeature } from "../types"; +import { HumidifierToggleCardFeatureConfig } from "./types"; + +export const supportsHumidifierToggleCardFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return domain === "humidifier"; +}; + +@customElement("hui-humidifier-toggle-card-feature") +class HuiHumidifierToggleCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: HumidifierEntity; + + @state() private _config?: HumidifierToggleCardFeatureConfig; + + @state() _currentState?: HumidifierState; + + static getStubConfig(): HumidifierToggleCardFeatureConfig { + return { + type: "humidifier-toggle", + }; + } + + public setConfig(config: HumidifierToggleCardFeatureConfig): 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._currentState = this.stateObj.state as HumidifierState; + } + } + + private async _valueChanged(ev: CustomEvent) { + const newState = (ev.detail as any).value as HumidifierState; + + if (newState === this.stateObj!.state) return; + + const oldState = this.stateObj!.state as HumidifierState; + this._currentState = newState; + + try { + await this._setState(newState); + } catch (err) { + this._currentState = oldState; + } + } + + private async _setState(newState: HumidifierState) { + await this.hass!.callService( + "humidifier", + newState === "on" ? "turn_on" : "turn_off", + { + entity_id: this.stateObj!.entity_id, + } + ); + } + + protected render(): TemplateResult | null { + if ( + !this._config || + !this.hass || + !this.stateObj || + !supportsHumidifierToggleCardFeature(this.stateObj) + ) { + return null; + } + + const color = stateColorCss(this.stateObj); + + const options = ["on", "off"].map((entityState) => ({ + value: entityState, + label: this.hass!.formatEntityState(this.stateObj!, entityState), + path: entityState === "on" ? mdiWaterPercent : mdiPower, + })); + + return html` +
+ + +
+ `; + } + + static get styles() { + return css` + 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-humidifier-toggle-card-feature": HuiHumidifierToggleCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index 2cb4ff6c3a..6bcaf27aaf 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -70,6 +70,12 @@ export interface WaterHeaterOperationModesCardFeatureConfig { export interface HumidifierModesCardFeatureConfig { type: "humidifier-modes"; + style?: "dropdown" | "icons"; + modes?: string[]; +} + +export interface HumidifierToggleCardFeatureConfig { + type: "humidifier-toggle"; } export const VACUUM_COMMANDS = [ @@ -105,6 +111,7 @@ export type LovelaceCardFeatureConfig = | CoverTiltPositionCardFeatureConfig | CoverTiltCardFeatureConfig | FanSpeedCardFeatureConfig + | HumidifierToggleCardFeatureConfig | HumidifierModesCardFeatureConfig | LawnMowerCommandsCardFeatureConfig | LightBrightnessCardFeatureConfig diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts index 5c756be599..ef69c0e49d 100644 --- a/src/panels/lovelace/cards/hui-humidifier-card.ts +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -51,7 +51,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { entity: foundEntities[0] || "", features: [ { - type: "humidifier-modes", + type: "humidifier-toggle", }, ], }; 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 960029ca8b..875a6f357c 100644 --- a/src/panels/lovelace/create-element/create-card-feature-element.ts +++ b/src/panels/lovelace/create-element/create-card-feature-element.ts @@ -7,6 +7,7 @@ import "../card-features/hui-cover-tilt-card-feature"; import "../card-features/hui-cover-tilt-position-card-feature"; import "../card-features/hui-fan-speed-card-feature"; import "../card-features/hui-humidifier-modes-card-feature"; +import "../card-features/hui-humidifier-toggle-card-feature"; import "../card-features/hui-lawn-mower-commands-card-feature"; import "../card-features/hui-light-brightness-card-feature"; import "../card-features/hui-light-color-temp-card-feature"; @@ -32,6 +33,7 @@ const TYPES: Set = new Set([ "cover-tilt", "fan-speed", "humidifier-modes", + "humidifier-toggle", "lawn-mower-commands", "light-brightness", "light-color-temp", 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 6eedd9816b..9405377c1a 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 @@ -29,6 +29,7 @@ import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover- import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature"; import { supportsCoverTiltPositionCardFeature } from "../../card-features/hui-cover-tilt-position-card-feature"; import { supportsFanSpeedCardFeature } from "../../card-features/hui-fan-speed-card-feature"; +import { supportsHumidifierToggleCardFeature } from "../../card-features/hui-humidifier-toggle-card-feature"; import { supportsHumidifierModesCardFeature } from "../../card-features/hui-humidifier-modes-card-feature"; import { supportsLawnMowerCommandCardFeature } from "../../card-features/hui-lawn-mower-commands-card-feature"; import { supportsLightBrightnessCardFeature } from "../../card-features/hui-light-brightness-card-feature"; @@ -55,6 +56,7 @@ const UI_FEATURE_TYPES = [ "cover-tilt", "fan-speed", "humidifier-modes", + "humidifier-toggle", "lawn-mower-commands", "light-brightness", "light-color-temp", @@ -72,6 +74,7 @@ const EDITABLES_FEATURE_TYPES = new Set([ "vacuum-commands", "alarm-modes", "climate-hvac-modes", + "humidifier-modes", "water-heater-operation-modes", "lawn-mower-commands", "climate-preset-modes", @@ -91,6 +94,7 @@ const SUPPORTS_FEATURE_TYPES: Record< "cover-tilt": supportsCoverTiltCardFeature, "fan-speed": supportsFanSpeedCardFeature, "humidifier-modes": supportsHumidifierModesCardFeature, + "humidifier-toggle": supportsHumidifierToggleCardFeature, "lawn-mower-commands": supportsLawnMowerCommandCardFeature, "light-brightness": supportsLightBrightnessCardFeature, "light-color-temp": supportsLightColorTempCardFeature, diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts index b7efdab80c..3e052d9a64 100644 --- a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -14,15 +14,22 @@ import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; import "../../../../components/ha-form/ha-form"; import type { SchemaUnion } from "../../../../components/ha-form/types"; import type { HomeAssistant } from "../../../../types"; -import type { HumidifierCardConfig } from "../../cards/types"; import { LovelaceCardFeatureConfig, LovelaceCardFeatureContext, } from "../../card-features/types"; +import type { HumidifierCardConfig } from "../../cards/types"; import type { LovelaceCardEditor } from "../../types"; +import "../hui-sub-element-editor"; import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { EditSubElementEvent, SubElementEditorConfig } from "../types"; import "./hui-card-features-editor"; +import type { FeatureType } from "./hui-card-features-editor"; + +const COMPATIBLE_FEATURES_TYPES: FeatureType[] = [ + "humidifier-modes", + "humidifier-toggle", +]; const cardConfigStruct = assign( baseLovelaceCardConfig, @@ -103,6 +110,7 @@ export class HuiHumidifierCardEditor + [ + { + name: "style", + selector: { + select: { + multiple: false, + mode: "list", + options: ["dropdown", "icons"].map((mode) => ({ + value: mode, + label: localize( + `ui.panel.lovelace.editor.features.types.humidifier-modes.style_list.${mode}` + ), + })), + }, + }, + }, + { + name: "modes", + selector: { + select: { + multiple: true, + mode: "list", + options: + stateObj?.attributes.available_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue(stateObj, "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: HumidifierModesCardFeatureConfig = { + style: "dropdown", + 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 "modes": + return this.hass!.localize( + `ui.panel.lovelace.editor.features.types.humidifier-modes.${schema.name}` + ); + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-humidifier-modes-card-feature-editor": HuiHumidifierModesCardFeatureEditor; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 66925e8255..41ed918e23 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5291,8 +5291,17 @@ }, "preset_modes": "Preset modes" }, + "humidifier-toggle": { + "label": "Humidifier toggle" + }, "humidifier-modes": { - "label": "Humidifier modes" + "label": "Humidifier 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%]" + }, + "modes": "Modes" }, "select-options": { "label": "Select options"