From 821cd7fe05557689b2be1ee062ad0893cfed4f7d Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 17 Aug 2023 17:19:46 +0200 Subject: [PATCH] Add operation modes to tile card (#17597) --- src/data/climate.ts | 16 +- src/data/water_heater.ts | 39 ++-- .../create-tile-feature-element.ts | 4 +- .../hui-tile-card-features-editor.ts | 5 + ...ter-operation-modes-tile-feature-editor.ts | 99 +++++++++++ ...ter-heater-operation-modes-tile-feature.ts | 167 ++++++++++++++++++ src/panels/lovelace/tile-features/types.ts | 9 +- src/translations/en.json | 8 +- 8 files changed, 316 insertions(+), 31 deletions(-) create mode 100644 src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-tile-feature-editor.ts create mode 100644 src/panels/lovelace/tile-features/hui-water-heater-operation-modes-tile-feature.ts diff --git a/src/data/climate.ts b/src/data/climate.ts index 5fa54cdcfe..b3b7087a3b 100644 --- a/src/data/climate.ts +++ b/src/data/climate.ts @@ -95,15 +95,13 @@ export const enum ClimateEntityFeature { AUX_HEAT = 64, } -const hvacModeOrdering: { [key in HvacMode]: number } = { - auto: 1, - heat_cool: 2, - heat: 3, - cool: 4, - dry: 5, - fan_only: 6, - off: 7, -}; +const hvacModeOrdering = HVAC_MODES.reduce( + (order, mode, index) => { + order[mode] = index; + return order; + }, + {} as Record +); export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) => hvacModeOrdering[mode1] - hvacModeOrdering[mode2]; diff --git a/src/data/water_heater.ts b/src/data/water_heater.ts index ebb656b7a5..84dac0a8f4 100644 --- a/src/data/water_heater.ts +++ b/src/data/water_heater.ts @@ -18,14 +18,17 @@ export const enum WaterHeaterEntityFeature { AWAY_MODE = 4, } -export type OperationMode = - | "eco" - | "electric" - | "performance" - | "high_demand" - | "heat_pump" - | "gas" - | "off"; +export const OPERATION_MODES = [ + "electric", + "gas", + "heat_pump", + "eco", + "performance", + "high_demand", + "off", +] as const; + +export type OperationMode = (typeof OPERATION_MODES)[number]; export type WaterHeaterEntity = HassEntityBase & { attributes: HassEntityAttributeBase & { @@ -40,20 +43,20 @@ export type WaterHeaterEntity = HassEntityBase & { }; }; -const hvacModeOrdering: { [key in OperationMode]: number } = { - eco: 1, - electric: 2, - performance: 3, - high_demand: 4, - heat_pump: 5, - gas: 6, - off: 7, -}; +const waterHeaterOperationModeOrdering = OPERATION_MODES.reduce( + (order, mode, index) => { + order[mode] = index; + return order; + }, + {} as Record +); export const compareWaterHeaterOperationMode = ( mode1: OperationMode, mode2: OperationMode -) => hvacModeOrdering[mode1] - hvacModeOrdering[mode2]; +) => + waterHeaterOperationModeOrdering[mode1] - + waterHeaterOperationModeOrdering[mode2]; export const WATER_HEATER_OPERATION_MODE_ICONS: Record = { 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 973b52ae42..5597f2cac2 100644 --- a/src/panels/lovelace/create-element/create-tile-feature-element.ts +++ b/src/panels/lovelace/create-element/create-tile-feature-element.ts @@ -1,10 +1,11 @@ import "../tile-features/hui-alarm-modes-tile-feature"; +import "../tile-features/hui-climate-hvac-modes-tile-feature"; import "../tile-features/hui-cover-open-close-tile-feature"; import "../tile-features/hui-cover-tilt-tile-feature"; import "../tile-features/hui-fan-speed-tile-feature"; import "../tile-features/hui-light-brightness-tile-feature"; import "../tile-features/hui-vacuum-commands-tile-feature"; -import "../tile-features/hui-climate-hvac-modes-tile-feature"; +import "../tile-features/hui-water-heater-operation-modes-tile-feature"; import { LovelaceTileFeatureConfig } from "../tile-features/types"; import { createLovelaceElement, @@ -19,6 +20,7 @@ const TYPES: Set = new Set([ "fan-speed", "alarm-modes", "climate-hvac-modes", + "water-heater-operation-modes", ]); export const createTileFeatureElement = (config: LovelaceTileFeatureConfig) => 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 a1c899bef4..e2b1a350c7 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 @@ -32,6 +32,7 @@ import { supportsCoverTiltTileFeature } from "../../tile-features/hui-cover-tilt import { supportsFanSpeedTileFeature } from "../../tile-features/hui-fan-speed-tile-feature"; import { supportsLightBrightnessTileFeature } from "../../tile-features/hui-light-brightness-tile-feature"; 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"; type FeatureType = LovelaceTileFeatureConfig["type"]; @@ -45,12 +46,14 @@ const FEATURE_TYPES: FeatureType[] = [ "fan-speed", "alarm-modes", "climate-hvac-modes", + "water-heater-operation-modes", ]; const EDITABLES_FEATURE_TYPES = new Set([ "vacuum-commands", "alarm-modes", "climate-hvac-modes", + "water-heater-operation-modes", ]); const SUPPORTS_FEATURE_TYPES: Record = @@ -62,6 +65,8 @@ const SUPPORTS_FEATURE_TYPES: Record = "fan-speed": supportsFanSpeedTileFeature, "alarm-modes": supportsAlarmModesTileFeature, "climate-hvac-modes": supportsClimateHvacModesTileFeature, + "water-heater-operation-modes": + supportsWaterHeaterOperationModesTileFeature, }; const CUSTOM_FEATURE_ENTRIES: Record< diff --git a/src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-tile-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-tile-feature-editor.ts new file mode 100644 index 0000000000..4cc6e7adae --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-water-heater-operation-modes-tile-feature-editor.ts @@ -0,0 +1,99 @@ +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 type { FormatEntityStateFunc } from "../../../../common/translations/entity-state"; +import "../../../../components/ha-form/ha-form"; +import type { SchemaUnion } from "../../../../components/ha-form/types"; +import type { HomeAssistant } from "../../../../types"; +import { + WaterHeaterOperationModesTileFeatureConfig, + LovelaceTileFeatureContext, +} from "../../tile-features/types"; +import type { LovelaceTileFeatureEditor } from "../../types"; +import { OPERATION_MODES } from "../../../../data/water_heater"; + +@customElement("hui-water-heater-operation-modes-tile-feature-editor") +export class HuiWaterHeaterOperationModesTileFeatureEditor + extends LitElement + implements LovelaceTileFeatureEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceTileFeatureContext; + + @state() private _config?: WaterHeaterOperationModesTileFeatureConfig; + + public setConfig(config: WaterHeaterOperationModesTileFeatureConfig): void { + this._config = config; + } + + private _schema = memoizeOne( + (formatEntityState: FormatEntityStateFunc, stateObj?: HassEntity) => + [ + { + name: "operation_modes", + selector: { + select: { + multiple: true, + mode: "list", + options: OPERATION_MODES.filter( + (mode) => stateObj?.attributes.operation_list?.includes(mode) + ).map((mode) => ({ + value: mode, + label: stateObj ? formatEntityState(stateObj, mode) : mode, + })), + }, + }, + }, + ] as const + ); + + protected render() { + if (!this.hass || !this._config) { + return nothing; + } + + const stateObj = this.context?.entity_id + ? this.hass.states[this.context?.entity_id] + : undefined; + + const schema = this._schema(this.hass.formatEntityState, stateObj); + + return html` + + `; + } + + private _valueChanged(ev: CustomEvent): void { + fireEvent(this, "config-changed", { config: ev.detail.value }); + } + + private _computeLabelCallback = ( + schema: SchemaUnion> + ) => { + switch (schema.name) { + case "operation_modes": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.tile.features.types.water-heater-modes.${schema.name}` + ); + default: + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-water-heater-operation-modes-tile-feature-editor": HuiWaterHeaterOperationModesTileFeatureEditor; + } +} diff --git a/src/panels/lovelace/tile-features/hui-water-heater-operation-modes-tile-feature.ts b/src/panels/lovelace/tile-features/hui-water-heater-operation-modes-tile-feature.ts new file mode 100644 index 0000000000..e537b60401 --- /dev/null +++ b/src/panels/lovelace/tile-features/hui-water-heater-operation-modes-tile-feature.ts @@ -0,0 +1,167 @@ +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 { computeDomain } from "../../../common/entity/compute_domain"; +import { stateColorCss } from "../../../common/entity/state_color"; +import "../../../components/ha-control-button"; +import "../../../components/ha-control-button-group"; +import "../../../components/ha-control-select"; +import type { ControlSelectOption } from "../../../components/ha-control-select"; +import "../../../components/ha-control-slider"; +import { UNAVAILABLE } from "../../../data/entity"; +import { + compareWaterHeaterOperationMode, + computeOperationModeIcon, + OperationMode, + WaterHeaterEntity, +} from "../../../data/water_heater"; +import { HomeAssistant } from "../../../types"; +import { LovelaceTileFeature, LovelaceTileFeatureEditor } from "../types"; +import { WaterHeaterOperationModesTileFeatureConfig } from "./types"; + +export const supportsWaterHeaterOperationModesTileFeature = ( + stateObj: HassEntity +) => { + const domain = computeDomain(stateObj.entity_id); + return domain === "water_heater"; +}; + +@customElement("hui-water-heater-operation-modes-tile-feature") +class HuiWaterHeaterOperationModeTileFeature + extends LitElement + implements LovelaceTileFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: WaterHeaterEntity; + + @state() private _config?: WaterHeaterOperationModesTileFeatureConfig; + + @state() _currentOperationMode?: OperationMode; + + static getStubConfig( + _, + stateObj?: HassEntity + ): WaterHeaterOperationModesTileFeatureConfig { + return { + type: "water-heater-operation-modes", + operation_modes: stateObj?.attributes.operation_list || [], + }; + } + + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-water-heater-operation-modes-tile-feature-editor" + ); + return document.createElement( + "hui-water-heater-operation-modes-tile-feature-editor" + ); + } + + public setConfig(config: WaterHeaterOperationModesTileFeatureConfig): 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._currentOperationMode = this.stateObj.state as OperationMode; + } + } + + private async _valueChanged(ev: CustomEvent) { + const mode = (ev.detail as any).value as OperationMode; + + if (mode === this.stateObj!.state) return; + + const oldMode = this.stateObj!.state as OperationMode; + this._currentOperationMode = mode; + + try { + await this._setMode(mode); + } 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.stateObj || + !supportsWaterHeaterOperationModesTileFeature(this.stateObj) + ) { + return null; + } + + const color = stateColorCss(this.stateObj); + + const modes = this._config.operation_modes || []; + + const options = modes + .filter((mode) => this.stateObj?.attributes.operation_list.includes(mode)) + .sort(compareWaterHeaterOperationMode) + .map((mode) => ({ + value: mode, + label: this.hass!.formatEntityState(this.stateObj!, mode), + path: computeOperationModeIcon(mode), + })); + + 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-water-heater-operation-modes-feature": HuiWaterHeaterOperationModeTileFeature; + } +} diff --git a/src/panels/lovelace/tile-features/types.ts b/src/panels/lovelace/tile-features/types.ts index ae1181e4b6..66313a881a 100644 --- a/src/panels/lovelace/tile-features/types.ts +++ b/src/panels/lovelace/tile-features/types.ts @@ -1,5 +1,6 @@ import { AlarmMode } from "../../../data/alarm_control_panel"; import { HvacMode } from "../../../data/climate"; +import { OperationMode } from "../../../data/water_heater"; export interface CoverOpenCloseTileFeatureConfig { type: "cover-open-close"; @@ -27,6 +28,11 @@ export interface ClimateHvacModesTileFeatureConfig { hvac_modes?: HvacMode[]; } +export interface WaterHeaterOperationModesTileFeatureConfig { + type: "water-heater-operation-modes"; + operation_modes?: OperationMode[]; +} + export const VACUUM_COMMANDS = [ "start_pause", "stop", @@ -49,7 +55,8 @@ export type LovelaceTileFeatureConfig = | VacuumCommandsTileFeatureConfig | FanSpeedTileFeatureConfig | AlarmModesTileFeatureConfig - | ClimateHvacModesTileFeatureConfig; + | ClimateHvacModesTileFeatureConfig + | WaterHeaterOperationModesTileFeatureConfig; export type LovelaceTileFeatureContext = { entity_id?: string; diff --git a/src/translations/en.json b/src/translations/en.json index 387a8af714..346fed4163 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5000,8 +5000,12 @@ } }, "climate-hvac-modes": { - "label": "HVAC modes", - "hvac_modes": "Modes" + "label": "Climate HVAC modes", + "hvac_modes": "HVAC modes" + }, + "water-heater-operation-modes": { + "label": "Water heater operation modes", + "operation_modes": "Operation modes" } } }