diff --git a/src/common/translations/entity-state.ts b/src/common/translations/entity-state.ts index 29073222c5..36d2e58332 100644 --- a/src/common/translations/entity-state.ts +++ b/src/common/translations/entity-state.ts @@ -3,25 +3,30 @@ import type { FrontendLocaleData } from "../../data/translation"; import type { HomeAssistant } from "../../types"; import type { LocalizeFunc } from "./localize"; -export type FormatEntityStateFunc = { - formatEntityState: (stateObj: HassEntity, state?: string) => string; - formatEntityAttributeValue: ( - stateObj: HassEntity, - attribute: string, - value?: any - ) => string; - formatEntityAttributeName: ( - stateObj: HassEntity, - attribute: string - ) => string; -}; +export type FormatEntityStateFunc = ( + stateObj: HassEntity, + state?: string +) => string; +export type FormatEntityAttributeValueFunc = ( + stateObj: HassEntity, + attribute: string, + value?: any +) => string; +export type formatEntityAttributeNameFunc = ( + stateObj: HassEntity, + attribute: string +) => string; export const computeFormatFunctions = async ( localize: LocalizeFunc, locale: FrontendLocaleData, config: HassConfig, entities: HomeAssistant["entities"] -): Promise => { +): Promise<{ + formatEntityState: FormatEntityStateFunc; + formatEntityAttributeValue: FormatEntityAttributeValueFunc; + formatEntityAttributeName: formatEntityAttributeNameFunc; +}> => { const { computeStateDisplay } = await import( "../entity/compute_state_display" ); diff --git a/src/data/climate.ts b/src/data/climate.ts index bf72c04582..5fa54cdcfe 100644 --- a/src/data/climate.ts +++ b/src/data/climate.ts @@ -34,14 +34,17 @@ import { import { haOscillatingOff } from "./icons/haOscillatingOff"; import { haOscillating } from "./icons/haOscillating"; -export type HvacMode = - | "off" - | "heat" - | "cool" - | "heat_cool" - | "auto" - | "dry" - | "fan_only"; +export const HVAC_MODES = [ + "auto", + "heat_cool", + "heat", + "cool", + "dry", + "fan_only", + "off", +] as const; + +export type HvacMode = (typeof HVAC_MODES)[number]; export const CLIMATE_PRESET_NONE = "none"; diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts index 95c6e37237..fed9699e55 100644 --- a/src/dialogs/more-info/controls/more-info-climate.ts +++ b/src/dialogs/more-info/controls/more-info-climate.ts @@ -58,7 +58,6 @@ class MoreInfoClimate extends LitElement { return nothing; } - const hass = this.hass; const stateObj = this.stateObj; const supportTargetHumidity = supportsFeature( @@ -167,7 +166,10 @@ class MoreInfoClimate extends LitElement {
= new Set([ "vacuum-commands", "fan-speed", "alarm-modes", + "climate-hvac-modes", ]); export const createTileFeatureElement = (config: LovelaceTileFeatureConfig) => diff --git a/src/panels/lovelace/editor/config-elements/hui-alarm-modes-tile-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-alarm-modes-tile-feature-editor.ts index 9793135ecd..e6ff299752 100644 --- a/src/panels/lovelace/editor/config-elements/hui-alarm-modes-tile-feature-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-alarm-modes-tile-feature-editor.ts @@ -10,7 +10,7 @@ import { AlarmMode, ALARM_MODES } from "../../../../data/alarm_control_panel"; import type { HomeAssistant } from "../../../../types"; import { LovelaceTileFeatureContext, - AlarmModesFileFeatureConfig, + AlarmModesTileFeatureConfig, } from "../../tile-features/types"; import type { LovelaceTileFeatureEditor } from "../../types"; import "../../../../components/ha-form/ha-form"; @@ -24,9 +24,9 @@ export class HuiAlarmModesTileFeatureEditor @property({ attribute: false }) public context?: LovelaceTileFeatureContext; - @state() private _config?: AlarmModesFileFeatureConfig; + @state() private _config?: AlarmModesTileFeatureConfig; - public setConfig(config: AlarmModesFileFeatureConfig): void { + public setConfig(config: AlarmModesTileFeatureConfig): void { this._config = config; } diff --git a/src/panels/lovelace/editor/config-elements/hui-climate-hvac-modes-tile-feature-editor.ts b/src/panels/lovelace/editor/config-elements/hui-climate-hvac-modes-tile-feature-editor.ts new file mode 100644 index 0000000000..a64faeb6f1 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-climate-hvac-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 { HVAC_MODES } from "../../../../data/climate"; +import type { HomeAssistant } from "../../../../types"; +import { + ClimateHvacModesTileFeatureConfig, + LovelaceTileFeatureContext, +} from "../../tile-features/types"; +import type { LovelaceTileFeatureEditor } from "../../types"; + +@customElement("hui-climate-hvac-modes-tile-feature-editor") +export class HuiClimateHvacModesTileFeatureEditor + extends LitElement + implements LovelaceTileFeatureEditor +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public context?: LovelaceTileFeatureContext; + + @state() private _config?: ClimateHvacModesTileFeatureConfig; + + public setConfig(config: ClimateHvacModesTileFeatureConfig): void { + this._config = config; + } + + private _schema = memoizeOne( + (formatEntityState: FormatEntityStateFunc, stateObj?: HassEntity) => + [ + { + name: "hvac_modes", + selector: { + select: { + multiple: true, + mode: "list", + options: HVAC_MODES.filter( + (mode) => stateObj?.attributes.hvac_modes?.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 "hvac_modes": + return this.hass!.localize( + `ui.panel.lovelace.editor.card.tile.features.types.climate-hvac-modes.${schema.name}` + ); + default: + return this.hass!.localize( + `ui.panel.lovelace.editor.card.generic.${schema.name}` + ); + } + }; +} + +declare global { + interface HTMLElementTagNameMap { + "hui-climate-hvac-modes-tile-feature-editor": HuiClimateHvacModesTileFeatureEditor; + } +} 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 712c9e1d26..a1c899bef4 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 @@ -1,6 +1,6 @@ import { mdiDelete, mdiDrag, mdiListBox, mdiPencil, mdiPlus } from "@mdi/js"; import { HassEntity } from "home-assistant-js-websocket"; -import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { repeat } from "lit/directives/repeat"; import type { SortableEvent } from "sortablejs"; @@ -12,20 +12,21 @@ import "../../../../components/ha-icon-button"; import "../../../../components/ha-list-item"; import "../../../../components/ha-svg-icon"; import { + CUSTOM_TYPE_PREFIX, CustomTileFeatureEntry, customTileFeatures, - CUSTOM_TYPE_PREFIX, isCustomType, stripCustomPrefix, } from "../../../../data/lovelace_custom_cards"; import { sortableStyles } from "../../../../resources/ha-sortable-style"; import { - loadSortable, SortableInstance, + loadSortable, } from "../../../../resources/sortable.ondemand"; 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 { supportsCoverOpenCloseTileFeature } from "../../tile-features/hui-cover-open-close-tile-feature"; import { supportsCoverTiltTileFeature } from "../../tile-features/hui-cover-tilt-tile-feature"; import { supportsFanSpeedTileFeature } from "../../tile-features/hui-fan-speed-tile-feature"; @@ -43,11 +44,13 @@ const FEATURE_TYPES: FeatureType[] = [ "vacuum-commands", "fan-speed", "alarm-modes", + "climate-hvac-modes", ]; const EDITABLES_FEATURE_TYPES = new Set([ "vacuum-commands", "alarm-modes", + "climate-hvac-modes", ]); const SUPPORTS_FEATURE_TYPES: Record = @@ -58,6 +61,7 @@ const SUPPORTS_FEATURE_TYPES: Record = "vacuum-commands": supportsVacuumCommandTileFeature, "fan-speed": supportsFanSpeedTileFeature, "alarm-modes": supportsAlarmModesTileFeature, + "climate-hvac-modes": supportsClimateHvacModesTileFeature, }; const CUSTOM_FEATURE_ENTRIES: Record< diff --git a/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts b/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts index 1675e1b913..e68e8201f3 100644 --- a/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts +++ b/src/panels/lovelace/tile-features/hui-alarm-modes-tile-feature.ts @@ -20,7 +20,7 @@ import { import { UNAVAILABLE } from "../../../data/entity"; import { HomeAssistant } from "../../../types"; import { LovelaceTileFeature, LovelaceTileFeatureEditor } from "../types"; -import { AlarmModesFileFeatureConfig } from "./types"; +import { AlarmModesTileFeatureConfig } from "./types"; import { showEnterCodeDialogDialog } from "../../../dialogs/enter-code/show-enter-code-dialog"; export const supportsAlarmModesTileFeature = (stateObj: HassEntity) => { @@ -37,11 +37,11 @@ class HuiAlarmModeTileFeature @property({ attribute: false }) public stateObj?: AlarmControlPanelEntity; - @state() private _config?: AlarmModesFileFeatureConfig; + @state() private _config?: AlarmModesTileFeatureConfig; @state() _currentMode?: AlarmMode; - static getStubConfig(_, stateObj?: HassEntity): AlarmModesFileFeatureConfig { + static getStubConfig(_, stateObj?: HassEntity): AlarmModesTileFeatureConfig { return { type: "alarm-modes", modes: stateObj @@ -60,7 +60,7 @@ class HuiAlarmModeTileFeature return document.createElement("hui-alarm-modes-tile-feature-editor"); } - public setConfig(config: AlarmModesFileFeatureConfig): void { + public setConfig(config: AlarmModesTileFeatureConfig): void { if (!config) { throw new Error("Invalid configuration"); } diff --git a/src/panels/lovelace/tile-features/hui-climate-hvac-modes-tile-feature.ts b/src/panels/lovelace/tile-features/hui-climate-hvac-modes-tile-feature.ts new file mode 100644 index 0000000000..c04f24c102 --- /dev/null +++ b/src/panels/lovelace/tile-features/hui-climate-hvac-modes-tile-feature.ts @@ -0,0 +1,163 @@ +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 { + ClimateEntity, + compareClimateHvacModes, + computeHvacModeIcon, + HvacMode, +} from "../../../data/climate"; +import { UNAVAILABLE } from "../../../data/entity"; +import { HomeAssistant } from "../../../types"; +import { LovelaceTileFeature, LovelaceTileFeatureEditor } from "../types"; +import { ClimateHvacModesTileFeatureConfig } from "./types"; + +export const supportsClimateHvacModesTileFeature = (stateObj: HassEntity) => { + const domain = computeDomain(stateObj.entity_id); + return domain === "climate"; +}; + +@customElement("hui-climate-hvac-modes-tile-feature") +class HuiClimateHvacModeTileFeature + extends LitElement + implements LovelaceTileFeature +{ + @property({ attribute: false }) public hass?: HomeAssistant; + + @property({ attribute: false }) public stateObj?: ClimateEntity; + + @state() private _config?: ClimateHvacModesTileFeatureConfig; + + @state() _currentHvacMode?: HvacMode; + + static getStubConfig( + _, + stateObj?: HassEntity + ): ClimateHvacModesTileFeatureConfig { + return { + type: "climate-hvac-modes", + hvac_modes: stateObj?.attributes.hvac_modes || [], + }; + } + + public static async getConfigElement(): Promise { + await import( + "../editor/config-elements/hui-climate-hvac-modes-tile-feature-editor" + ); + return document.createElement("hui-climate-hvac-modes-tile-feature-editor"); + } + + public setConfig(config: ClimateHvacModesTileFeatureConfig): 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._currentHvacMode = this.stateObj.state as HvacMode; + } + } + + private async _valueChanged(ev: CustomEvent) { + const mode = (ev.detail as any).value as HvacMode; + + if (mode === this.stateObj!.state) return; + + const oldMode = this.stateObj!.state as HvacMode; + this._currentHvacMode = mode; + + try { + await this._setMode(mode); + } catch (err) { + this._currentHvacMode = oldMode; + } + } + + private async _setMode(mode: HvacMode) { + await this.hass!.callService("climate", "set_hvac_mode", { + entity_id: this.stateObj!.entity_id, + hvac_mode: mode, + }); + } + + protected render(): TemplateResult | null { + if ( + !this._config || + !this.hass || + !this.stateObj || + !supportsClimateHvacModesTileFeature(this.stateObj) + ) { + return null; + } + + const color = stateColorCss(this.stateObj); + + const modes = this._config.hvac_modes || []; + + const options = modes + .filter((mode) => this.stateObj?.attributes.hvac_modes.includes(mode)) + .sort(compareClimateHvacModes) + .map((mode) => ({ + value: mode, + label: this.hass!.formatEntityState(this.stateObj!, mode), + path: computeHvacModeIcon(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-climate-modes-hvac-modes-feature": HuiClimateHvacModeTileFeature; + } +} diff --git a/src/panels/lovelace/tile-features/types.ts b/src/panels/lovelace/tile-features/types.ts index 03128a5efb..ae1181e4b6 100644 --- a/src/panels/lovelace/tile-features/types.ts +++ b/src/panels/lovelace/tile-features/types.ts @@ -1,4 +1,5 @@ import { AlarmMode } from "../../../data/alarm_control_panel"; +import { HvacMode } from "../../../data/climate"; export interface CoverOpenCloseTileFeatureConfig { type: "cover-open-close"; @@ -16,11 +17,16 @@ export interface FanSpeedTileFeatureConfig { type: "fan-speed"; } -export interface AlarmModesFileFeatureConfig { +export interface AlarmModesTileFeatureConfig { type: "alarm-modes"; modes?: AlarmMode[]; } +export interface ClimateHvacModesTileFeatureConfig { + type: "climate-hvac-modes"; + hvac_modes?: HvacMode[]; +} + export const VACUUM_COMMANDS = [ "start_pause", "stop", @@ -42,7 +48,8 @@ export type LovelaceTileFeatureConfig = | LightBrightnessTileFeatureConfig | VacuumCommandsTileFeatureConfig | FanSpeedTileFeatureConfig - | AlarmModesFileFeatureConfig; + | AlarmModesTileFeatureConfig + | ClimateHvacModesTileFeatureConfig; export type LovelaceTileFeatureContext = { entity_id?: string; diff --git a/src/translations/en.json b/src/translations/en.json index c6cc34a782..387a8af714 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4998,6 +4998,10 @@ "locate": "[%key:ui::dialogs::more_info_control::vacuum::locate%]", "return_home": "[%key:ui::dialogs::more_info_control::vacuum::return_home%]" } + }, + "climate-hvac-modes": { + "label": "HVAC modes", + "hvac_modes": "Modes" } } }