From 90d01e4b63ae63cb0cba9ce58cb3f7b7d4c8c08f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 21 Sep 2023 18:41:09 +0200 Subject: [PATCH] Add style and preset modes options to climate preset tile feature (#17977) --- .../create-tile-feature-element.ts | 4 +- ...limate-preset-modes-tile-feature-editor.ts | 133 +++++++++++ .../hui-tile-card-features-editor.ts | 54 +++-- .../hui-climate-preset-modes-tile-feature.ts | 222 ++++++++++++++++++ .../hui-climate-presets-tile-feature.ts | 140 ----------- .../hui-select-options-tile-feature.ts | 5 +- src/panels/lovelace/tile-features/types.ts | 8 +- src/translations/en.json | 13 +- 8 files changed, 404 insertions(+), 175 deletions(-) create mode 100644 src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-tile-feature-editor.ts create mode 100644 src/panels/lovelace/tile-features/hui-climate-preset-modes-tile-feature.ts delete mode 100644 src/panels/lovelace/tile-features/hui-climate-presets-tile-feature.ts diff --git a/src/panels/lovelace/create-element/create-tile-feature-element.ts b/src/panels/lovelace/create-element/create-tile-feature-element.ts index c40aa421037b..b0cc259cb2ac 100644 --- a/src/panels/lovelace/create-element/create-tile-feature-element.ts +++ b/src/panels/lovelace/create-element/create-tile-feature-element.ts @@ -1,6 +1,6 @@ import "../tile-features/hui-alarm-modes-tile-feature"; import "../tile-features/hui-climate-hvac-modes-tile-feature"; -import "../tile-features/hui-climate-presets-tile-feature"; +import "../tile-features/hui-climate-preset-modes-tile-feature"; import "../tile-features/hui-cover-open-close-tile-feature"; import "../tile-features/hui-cover-position-tile-feature"; import "../tile-features/hui-cover-tilt-position-tile-feature"; @@ -22,7 +22,7 @@ import { const TYPES: Set = new Set([ "alarm-modes", "climate-hvac-modes", - "climate-presets", + "climate-preset-modes", "cover-open-close", "cover-position", "cover-tilt-position", diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-tile-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-tile-feature-editor.ts new file mode 100644 index 000000000000..6807fcae2722 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-climate-preset-modes-tile-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 { + ClimatePresetModesTileFeatureConfig, + LovelaceTileFeatureContext, +} from "../../tile-features/types"; +import type { LovelaceTileFeatureEditor } from "../../types"; + +@customElement("hui-climate-preset-modes-tile-feature-editor") +export class HuiClimatePresetModesTileFeatureEditor + extends LitElement + implements LovelaceTileFeatureEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceTileFeatureContext; + + @state() private _config?: ClimatePresetModesTileFeatureConfig; + + public setConfig(config: ClimatePresetModesTileFeatureConfig): 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.card.tile.features.types.climate-preset-modes.style_list.${mode}` + ), + })), + }, + }, + }, + { + name: "preset_modes", + selector: { + select: { + multiple: true, + mode: "list", + options: + stateObj?.attributes.preset_modes?.map((mode) => ({ + value: mode, + label: formatEntityAttributeValue( + stateObj, + "preset_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: ClimatePresetModesTileFeatureConfig = { + style: "dropdown", + preset_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 "preset_modes": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.tile.features.types.climate-preset-modes.${schema.name}` + ); + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-climate-preset-modes-tile-feature-editor": HuiClimatePresetModesTileFeatureEditor; + } +} diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts index cdb420d230a0..f35b32893ffc 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts @@ -27,7 +27,6 @@ import { HomeAssistant } from "../../../../types"; import { getTileFeatureElementClass } from "../../create-element/create-tile-feature-element"; import { supportsAlarmModesTileFeature } from "../../tile-features/hui-alarm-modes-tile-feature"; import { supportsClimateHvacModesTileFeature } from "../../tile-features/hui-climate-hvac-modes-tile-feature"; -import { supportsClimatePresetsTileFeature } from "../../tile-features/hui-climate-presets-tile-feature"; import { supportsCoverOpenCloseTileFeature } from "../../tile-features/hui-cover-open-close-tile-feature"; import { supportsCoverPositionTileFeature } from "../../tile-features/hui-cover-position-tile-feature"; import { supportsCoverTiltPositionTileFeature } from "../../tile-features/hui-cover-tilt-position-tile-feature"; @@ -41,14 +40,15 @@ import { supportsTargetTemperatureTileFeature } from "../../tile-features/hui-ta import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature"; import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-tile-feature"; import { LovelaceTileFeatureConfig } from "../../tile-features/types"; +import { supportsClimatePresetModesTileFeature } from "../../tile-features/hui-climate-preset-modes-tile-feature"; type FeatureType = LovelaceTileFeatureConfig["type"]; type SupportsFeature = (stateObj: HassEntity) => boolean; -const FEATURE_TYPES: FeatureType[] = [ +const UI_FEATURE_TYPES = [ "alarm-modes", "climate-hvac-modes", - "climate-presets", + "climate-preset-modes", "cover-open-close", "cover-position", "cover-tilt-position", @@ -61,35 +61,39 @@ const FEATURE_TYPES: FeatureType[] = [ "target-temperature", "vacuum-commands", "water-heater-operation-modes", -]; +] as const satisfies readonly FeatureType[]; -const EDITABLES_FEATURE_TYPES = new Set([ +type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number]; + +const EDITABLES_FEATURE_TYPES = new Set([ "vacuum-commands", "alarm-modes", "climate-hvac-modes", "water-heater-operation-modes", "lawn-mower-commands", + "climate-preset-modes", ]); -const SUPPORTS_FEATURE_TYPES: Record = - { - "alarm-modes": supportsAlarmModesTileFeature, - "climate-hvac-modes": supportsClimateHvacModesTileFeature, - "climate-presets": supportsClimatePresetsTileFeature, - "cover-open-close": supportsCoverOpenCloseTileFeature, - "cover-position": supportsCoverPositionTileFeature, - "cover-tilt-position": supportsCoverTiltPositionTileFeature, - "cover-tilt": supportsCoverTiltTileFeature, - "fan-speed": supportsFanSpeedTileFeature, - "lawn-mower-commands": supportsLawnMowerCommandTileFeature, - "light-brightness": supportsLightBrightnessTileFeature, - "light-color-temp": supportsLightColorTempTileFeature, - "target-temperature": supportsTargetTemperatureTileFeature, - "vacuum-commands": supportsVacuumCommandTileFeature, - "water-heater-operation-modes": - supportsWaterHeaterOperationModesTileFeature, - "select-options": supportsSelectOptionTileFeature, - }; +const SUPPORTS_FEATURE_TYPES: Record< + UiFeatureTypes, + SupportsFeature | undefined +> = { + "alarm-modes": supportsAlarmModesTileFeature, + "climate-hvac-modes": supportsClimateHvacModesTileFeature, + "climate-preset-modes": supportsClimatePresetModesTileFeature, + "cover-open-close": supportsCoverOpenCloseTileFeature, + "cover-position": supportsCoverPositionTileFeature, + "cover-tilt-position": supportsCoverTiltPositionTileFeature, + "cover-tilt": supportsCoverTiltTileFeature, + "fan-speed": supportsFanSpeedTileFeature, + "lawn-mower-commands": supportsLawnMowerCommandTileFeature, + "light-brightness": supportsLightBrightnessTileFeature, + "light-color-temp": supportsLightColorTempTileFeature, + "target-temperature": supportsTargetTemperatureTileFeature, + "vacuum-commands": supportsVacuumCommandTileFeature, + "water-heater-operation-modes": supportsWaterHeaterOperationModesTileFeature, + "select-options": supportsSelectOptionTileFeature, +}; const CUSTOM_FEATURE_ENTRIES: Record< string, @@ -181,7 +185,7 @@ export class HuiTileCardFeaturesEditor extends LitElement { } private _getSupportedFeaturesType() { - const featuresTypes = FEATURE_TYPES as string[]; + const featuresTypes = UI_FEATURE_TYPES as readonly string[]; const customFeaturesTypes = customTileFeatures.map( (feature) => `${CUSTOM_TYPE_PREFIX}${feature.type}` ); diff --git a/src/panels/lovelace/tile-features/hui-climate-preset-modes-tile-feature.ts b/src/panels/lovelace/tile-features/hui-climate-preset-modes-tile-feature.ts new file mode 100644 index 000000000000..1879b9307377 --- /dev/null +++ b/src/panels/lovelace/tile-features/hui-climate-preset-modes-tile-feature.ts @@ -0,0 +1,222 @@ +import { mdiTuneVariant } 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-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, + computePresetModeIcon, +} from "../../../data/climate"; +import { UNAVAILABLE } from "../../../data/entity"; +import { HomeAssistant } from "../../../types"; +import { LovelaceTileFeature, LovelaceTileFeatureEditor } from "../types"; +import { ClimatePresetModesTileFeatureConfig } from "./types"; + +export const supportsClimatePresetModesTileFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return ( + domain === "climate" && + supportsFeature(stateObj, ClimateEntityFeature.PRESET_MODE) + ); +}; + +@customElement("hui-climate-preset-modes-tile-feature") +class HuiClimatePresetModeTileFeature + extends LitElement + implements LovelaceTileFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: ClimateEntity; + + @state() private _config?: ClimatePresetModesTileFeatureConfig; + + @state() _currentPresetMode?: string; + + @query("ha-control-select-menu", true) + private _haSelect?: HaControlSelectMenu; + + static getStubConfig( + _, + stateObj?: HassEntity + ): ClimatePresetModesTileFeatureConfig { + return { + type: "climate-preset-modes", + style: "dropdown", + preset_modes: stateObj?.attributes.preset_modes || [], + }; + } + + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-climate-preset-modes-tile-feature-editor" + ); + return document.createElement( + "hui-climate-preset-modes-tile-feature-editor" + ); + } + + public setConfig(config: ClimatePresetModesTileFeatureConfig): 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._currentPresetMode = this.stateObj.attributes.preset_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 presetMode = + (ev.detail as any).value ?? ((ev.target as any).value as string); + + const oldPresetMode = this.stateObj!.attributes.preset_mode; + + if (presetMode === oldPresetMode) 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.stateObj || + !supportsClimatePresetModesTileFeature(this.stateObj) + ) { + return null; + } + + const stateObj = this.stateObj; + + const modes = stateObj.attributes.preset_modes || []; + + const options = modes + .filter((mode) => (this._config!.preset_modes || []).includes(mode)) + .map((mode) => ({ + value: mode, + label: this.hass!.formatEntityAttributeValue( + this.stateObj!, + "preset_mode", + mode + ), + path: computePresetModeIcon(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(--tile-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-modes-preset-modes-feature": HuiClimatePresetModeTileFeature; + } +} diff --git a/src/panels/lovelace/tile-features/hui-climate-presets-tile-feature.ts b/src/panels/lovelace/tile-features/hui-climate-presets-tile-feature.ts deleted file mode 100644 index 4dd81fd00788..000000000000 --- a/src/panels/lovelace/tile-features/hui-climate-presets-tile-feature.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { HassEntity } from "home-assistant-js-websocket"; -import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; -import { customElement, property, state } from "lit/decorators"; -import { styleMap } from "lit/directives/style-map"; -import { stateColorCss } from "../../../common/entity/state_color"; -import { ClimateEntity, computePresetModeIcon } from "../../../data/climate"; -import { UNAVAILABLE } from "../../../data/entity"; -import { HomeAssistant } from "../../../types"; -import { LovelaceTileFeature } from "../types"; -import { ClimatePresetsTileFeatureConfig } from "./types"; -import type { ControlSelectOption } from "../../../components/ha-control-select"; -import { computeDomain } from "../../../common/entity/compute_domain"; -import "../../../components/ha-control-button"; -import "../../../components/ha-control-button-group"; -import "../../../components/ha-control-select"; -import "../../../components/ha-control-slider"; - -export const supportsClimatePresetsTileFeature = (stateObj: HassEntity) => { - const domain = computeDomain(stateObj.entity_id); - return domain === "climate"; -}; - -@customElement("hui-climate-presets-tile-feature") -class HuiClimatePresetsTileFeature - extends LitElement - implements LovelaceTileFeature -{ - @property({ attribute: false }) public hass?: HomeAssistant; - - @property({ attribute: false }) public stateObj?: ClimateEntity; - - @state() private _config?: ClimatePresetsTileFeatureConfig; - - @state() _currentPreset?: string; - - static getStubConfig(): ClimatePresetsTileFeatureConfig { - return { - type: "climate-presets", - }; - } - - public setConfig(config: ClimatePresetsTileFeatureConfig): 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._currentPreset = this.stateObj.attributes.preset_mode as string; - } - } - - private async _valueChanged(ev: CustomEvent) { - const preset = (ev.detail as any).value as string; - const oldPreset = this.stateObj!.attributes.preset_mode; - - if (preset === oldPreset) return; - this._currentPreset = preset; - - try { - await this._setPreset(preset); - } catch (err) { - this._currentPreset = oldPreset; - } - } - - private async _setPreset(preset: string) { - await this.hass!.callService("climate", "set_preset_mode", { - entity_id: this.stateObj!.entity_id, - preset_mode: preset, - }); - } - - protected render(): TemplateResult | null { - if ( - !this._config || - !this.hass || - !this.stateObj || - !supportsClimatePresetsTileFeature(this.stateObj) - ) { - return null; - } - - const color = stateColorCss(this.stateObj); - - const presets = (this.stateObj.attributes.preset_modes as string[]) || []; - - const options = presets.map((preset) => ({ - value: preset, - label: this.hass!.formatEntityAttributeName(this.stateObj!, preset), - path: computePresetModeIcon(preset), - })); - - return html` -
- - -
- `; - } - - static get styles() { - return css` - ha-control-select { - --control-select-color: var(--tile-color); - --control-select-padding: 0; - --control-select-thickness: 40px; - --control-select-border-radius: 10px; - --control-select-button-border-radius: 10px; - } - ha-control-button-group { - margin: 0 12px 12px 12px; - --control-button-group-spacing: 12px; - } - .container { - padding: 0 12px 12px 12px; - width: auto; - } - `; - } -} - -declare global { - interface HTMLElementTagNameMap { - "hui-climate-preset-tile-feature": HuiClimatePresetsTileFeature; - } -} diff --git a/src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts b/src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts index 13c2fb956722..48c2b3eb9622 100644 --- a/src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts +++ b/src/panels/lovelace/tile-features/hui-select-options-tile-feature.ts @@ -72,9 +72,10 @@ class HuiSelectOptionsTileFeature private async _valueChanged(ev: CustomEvent) { const option = (ev.target as any).value as string; - if (option === this.stateObj!.state) return; - const oldOption = this.stateObj!.state; + + if (option === oldOption) return; + this._currentOption = option; try { diff --git a/src/panels/lovelace/tile-features/types.ts b/src/panels/lovelace/tile-features/types.ts index 9a6670bf528d..893791f235d9 100644 --- a/src/panels/lovelace/tile-features/types.ts +++ b/src/panels/lovelace/tile-features/types.ts @@ -40,8 +40,10 @@ export interface ClimateHvacModesTileFeatureConfig { hvac_modes?: HvacMode[]; } -export interface ClimatePresetsTileFeatureConfig { - type: "climate-presets"; +export interface ClimatePresetModesTileFeatureConfig { + type: "climate-preset-modes"; + style?: "dropdown" | "icons"; + preset_modes?: string[]; } export interface SelectOptionsTileFeatureConfig { @@ -84,7 +86,7 @@ export interface LawnMowerCommandsTileFeatureConfig { export type LovelaceTileFeatureConfig = | AlarmModesTileFeatureConfig | ClimateHvacModesTileFeatureConfig - | ClimatePresetsTileFeatureConfig + | ClimatePresetModesTileFeatureConfig | CoverOpenCloseTileFeatureConfig | CoverPositionTileFeatureConfig | CoverTiltPositionTileFeatureConfig diff --git a/src/translations/en.json b/src/translations/en.json index a0a3103b988d..73df803ff01a 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5038,8 +5038,14 @@ "label": "Climate HVAC modes", "hvac_modes": "HVAC modes" }, - "climate-presets": { - "label": "Climate Presets" + "climate-preset-modes": { + "label": "Climate preset modes", + "style": "Style", + "style_list": { + "dropdown": "Dropdown", + "icons": "Icons" + }, + "preset_modes": "Preset modes" }, "target-temperature": { "label": "Target temperature" @@ -5142,7 +5148,8 @@ "types": { "header": "Header editor", "footer": "Footer editor", - "row": "Entity row editor" + "row": "Entity row editor", + "tile-feature": "Tile feature editor" } } },