diff --git a/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts new file mode 100644 index 0000000000..b4701dab84 --- /dev/null +++ b/src/panels/lovelace/card-features/hui-fan-preset-modes-card-feature.ts @@ -0,0 +1,231 @@ +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-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 { FanEntity, FanEntityFeature } from "../../../data/fan"; +import { UNAVAILABLE } from "../../../data/entity"; +import { HomeAssistant } from "../../../types"; +import { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; +import { FanPresetModesCardFeatureConfig } from "./types"; + +export const supportsFanPresetModesCardFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return ( + domain === "fan" && supportsFeature(stateObj, FanEntityFeature.PRESET_MODE) + ); +}; + +@customElement("hui-fan-preset-modes-card-feature") +class HuiFanPresetModesCardFeature + extends LitElement + implements LovelaceCardFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: FanEntity; + + @state() private _config?: FanPresetModesCardFeatureConfig; + + @state() _currentPresetMode?: string; + + @query("ha-control-select-menu", true) + private _haSelect?: HaControlSelectMenu; + + static getStubConfig( + _, + stateObj?: HassEntity + ): FanPresetModesCardFeatureConfig { + return { + type: "fan-preset-modes", + style: "dropdown", + preset_modes: stateObj?.attributes.preset_modes || [], + }; + } + + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-fan-preset-modes-card-feature-editor" + ); + return document.createElement("hui-fan-preset-modes-card-feature-editor"); + } + + public setConfig(config: FanPresetModesCardFeatureConfig): 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("fan", "set_preset_mode", { + entity_id: this.stateObj!.entity_id, + preset_mode: mode, + }); + } + + protected render(): TemplateResult | null { + if ( + !this._config || + !this.hass || + !this.stateObj || + !supportsFanPresetModesCardFeature(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 + ), + icon: html``, + })); + + if (this._config.style === "icons") { + return html` +
+ + +
+ `; + } + + return html` +
+ + ${this._currentPresetMode + ? 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-fan-preset-modes-card-feature": HuiFanPresetModesCardFeature; + } +} diff --git a/src/panels/lovelace/card-features/types.ts b/src/panels/lovelace/card-features/types.ts index e5c4d38858..f96427b4ba 100644 --- a/src/panels/lovelace/card-features/types.ts +++ b/src/panels/lovelace/card-features/types.ts @@ -26,6 +26,12 @@ export interface LightColorTempCardFeatureConfig { type: "light-color-temp"; } +export interface FanPresetModesCardFeatureConfig { + type: "fan-preset-modes"; + style?: "dropdown" | "icons"; + preset_modes?: string[]; +} + export interface FanSpeedCardFeatureConfig { type: "fan-speed"; } @@ -123,19 +129,20 @@ export type LovelaceCardFeatureConfig = | CoverPositionCardFeatureConfig | CoverTiltPositionCardFeatureConfig | CoverTiltCardFeatureConfig + | FanPresetModesCardFeatureConfig | FanSpeedCardFeatureConfig | HumidifierToggleCardFeatureConfig | HumidifierModesCardFeatureConfig | LawnMowerCommandsCardFeatureConfig | LightBrightnessCardFeatureConfig | LightColorTempCardFeatureConfig - | VacuumCommandsCardFeatureConfig + | NumericInputCardFeatureConfig + | SelectOptionsCardFeatureConfig | TargetHumidityCardFeatureConfig | TargetTemperatureCardFeatureConfig - | WaterHeaterOperationModesCardFeatureConfig - | SelectOptionsCardFeatureConfig - | NumericInputCardFeatureConfig - | UpdateActionsCardFeatureConfig; + | UpdateActionsCardFeatureConfig + | VacuumCommandsCardFeatureConfig + | WaterHeaterOperationModesCardFeatureConfig; export type LovelaceCardFeatureContext = { entity_id?: string; 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 5508221430..8aec1360e3 100644 --- a/src/panels/lovelace/create-element/create-card-feature-element.ts +++ b/src/panels/lovelace/create-element/create-card-feature-element.ts @@ -6,6 +6,7 @@ import "../card-features/hui-cover-open-close-card-feature"; import "../card-features/hui-cover-position-card-feature"; import "../card-features/hui-cover-tilt-card-feature"; import "../card-features/hui-cover-tilt-position-card-feature"; +import "../card-features/hui-fan-preset-modes-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"; @@ -16,9 +17,9 @@ import "../card-features/hui-numeric-input-card-feature"; import "../card-features/hui-select-options-card-feature"; import "../card-features/hui-target-temperature-card-feature"; import "../card-features/hui-target-humidity-card-feature"; +import "../card-features/hui-update-actions-card-feature"; import "../card-features/hui-vacuum-commands-card-feature"; import "../card-features/hui-water-heater-operation-modes-card-feature"; -import "../card-features/hui-update-actions-card-feature"; import { LovelaceCardFeatureConfig } from "../card-features/types"; import { @@ -35,6 +36,7 @@ const TYPES: Set = new Set([ "cover-position", "cover-tilt-position", "cover-tilt", + "fan-preset-modes", "fan-speed", "humidifier-modes", "humidifier-toggle", @@ -45,9 +47,9 @@ const TYPES: Set = new Set([ "select-options", "target-humidity", "target-temperature", + "update-actions", "vacuum-commands", "water-heater-operation-modes", - "update-actions", ]); export const createCardFeatureElement = (config: LovelaceCardFeatureConfig) => 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 d8fe23d0a7..ef1fcdfae0 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 @@ -27,6 +27,7 @@ import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature"; import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature"; import { supportsCoverTiltPositionCardFeature } from "../../card-features/hui-cover-tilt-position-card-feature"; +import { supportsFanPresetModesCardFeature } from "../../card-features/hui-fan-preset-modes-card-feature"; import { supportsFanSpeedCardFeature } from "../../card-features/hui-fan-speed-card-feature"; import { supportsHumidifierModesCardFeature } from "../../card-features/hui-humidifier-modes-card-feature"; import { supportsHumidifierToggleCardFeature } from "../../card-features/hui-humidifier-toggle-card-feature"; @@ -55,34 +56,36 @@ const UI_FEATURE_TYPES = [ "cover-position", "cover-tilt-position", "cover-tilt", + "fan-preset-modes", "fan-speed", "humidifier-modes", "humidifier-toggle", "lawn-mower-commands", "light-brightness", "light-color-temp", + "numeric-input", "select-options", "target-humidity", "target-temperature", - "vacuum-commands", "update-actions", + "vacuum-commands", "water-heater-operation-modes", - "numeric-input", ] as const satisfies readonly FeatureType[]; type UiFeatureTypes = (typeof UI_FEATURE_TYPES)[number]; const EDITABLES_FEATURE_TYPES = new Set([ - "vacuum-commands", "alarm-modes", "climate-hvac-modes", - "humidifier-modes", - "water-heater-operation-modes", - "lawn-mower-commands", "climate-fan-modes", "climate-preset-modes", + "fan-preset-modes", + "humidifier-modes", + "lawn-mower-commands", "numeric-input", "update-actions", + "vacuum-commands", + "water-heater-operation-modes", ]); const SUPPORTS_FEATURE_TYPES: Record< @@ -97,6 +100,7 @@ const SUPPORTS_FEATURE_TYPES: Record< "cover-position": supportsCoverPositionCardFeature, "cover-tilt-position": supportsCoverTiltPositionCardFeature, "cover-tilt": supportsCoverTiltCardFeature, + "fan-preset-modes": supportsFanPresetModesCardFeature, "fan-speed": supportsFanSpeedCardFeature, "humidifier-modes": supportsHumidifierModesCardFeature, "humidifier-toggle": supportsHumidifierToggleCardFeature, @@ -104,12 +108,12 @@ const SUPPORTS_FEATURE_TYPES: Record< "light-brightness": supportsLightBrightnessCardFeature, "light-color-temp": supportsLightColorTempCardFeature, "numeric-input": supportsNumericInputCardFeature, + "select-options": supportsSelectOptionsCardFeature, "target-humidity": supportsTargetHumidityCardFeature, "target-temperature": supportsTargetTemperatureCardFeature, + "update-actions": supportsUpdateActionsCardFeature, "vacuum-commands": supportsVacuumCommandsCardFeature, "water-heater-operation-modes": supportsWaterHeaterOperationModesCardFeature, - "select-options": supportsSelectOptionsCardFeature, - "update-actions": supportsUpdateActionsCardFeature, }; const customCardFeatures = getCustomCardFeatures(); diff --git a/src/panels/lovelace/editor/config-elements/hui-fan-preset-modes-card-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-fan-preset-modes-card-feature-editor.ts new file mode 100644 index 0000000000..35cfd7b8a6 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-fan-preset-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 { + FanPresetModesCardFeatureConfig, + LovelaceCardFeatureContext, +} from "../../card-features/types"; +import type { LovelaceCardFeatureEditor } from "../../types"; + +@customElement("hui-fan-preset-modes-card-feature-editor") +export class HuiFanPresetModesCardFeatureEditor + extends LitElement + implements LovelaceCardFeatureEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceCardFeatureContext; + + @state() private _config?: FanPresetModesCardFeatureConfig; + + public setConfig(config: FanPresetModesCardFeatureConfig): 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.fan-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: FanPresetModesCardFeatureConfig = { + 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.features.types.fan-preset-modes.${schema.name}` + ); + default: + return ""; + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-fan-preset-modes-card-feature-editor": HuiFanPresetModesCardFeatureEditor; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index aeb83fbbb4..ae81ca6a75 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5615,6 +5615,15 @@ }, "preset_modes": "Preset modes" }, + "fan-preset-modes": { + "label": "Fan preset 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%]" + }, + "preset_modes": "[%key:ui::panel::lovelace::editor::features::types::climate-preset-modes::preset_modes%]" + }, "humidifier-toggle": { "label": "Humidifier toggle" },