Skip to content

Commit 988fa3e

Browse files
Add horizontal swing to climate (#22043)
1 parent f9118a4 commit 988fa3e

File tree

11 files changed

+489
-6
lines changed

11 files changed

+489
-6
lines changed

gallery/src/pages/lovelace/thermostat-card.ts

+10-6
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,11 @@ const ENTITIES = [
8686
friendly_name: "Sensibo purifier",
8787
fan_modes: ["low", "high"],
8888
fan_mode: "low",
89-
swing_modes: ["on", "off", "both", "vertical", "horizontal"],
90-
swing_mode: "vertical",
91-
supported_features: 41,
89+
swing_modes: ["both", "rangefull", "off"],
90+
swing_mode: "rangefull",
91+
swing_horizontal_modes: ["both", "rangefull", "off"],
92+
swing_horizontal_mode: "both",
93+
supported_features: 553,
9294
}),
9395
getEntity("climate", "unavailable", "unavailable", {
9496
supported_features: 43,
@@ -188,11 +190,13 @@ const CONFIGS = [
188190
- type: climate-swing-modes
189191
style: icons
190192
swing_modes:
191-
- 'on'
193+
- 'both'
194+
- 'rangefull'
192195
- 'off'
196+
swing_horizontal_modes:
193197
- 'both'
194-
- 'vertical'
195-
- 'horizontal'
198+
- 'rangefull'
199+
- 'off'
196200
`,
197201
},
198202
{

gallery/src/pages/lovelace/tile-card.ts

+3
Original file line numberDiff line numberDiff line change
@@ -78,16 +78,19 @@ const ENTITIES = [
7878
fan_modes: ["on_low", "on_high", "auto_low", "auto_high", "off"],
7979
preset_modes: ["home", "eco", "away"],
8080
swing_modes: ["auto", "1", "2", "3", "off"],
81+
switch_horizontal_modes: ["auto", "4", "5", "6", "off"],
8182
current_temperature: 23,
8283
target_temp_high: 24,
8384
target_temp_low: 21,
8485
fan_mode: "auto_low",
8586
preset_mode: "home",
8687
swing_mode: "auto",
88+
swing_horizontal_mode: "off",
8789
supported_features:
8890
ClimateEntityFeature.TURN_ON +
8991
ClimateEntityFeature.TURN_OFF +
9092
ClimateEntityFeature.SWING_MODE +
93+
ClimateEntityFeature.SWING_HORIZONTAL_MODE +
9194
ClimateEntityFeature.PRESET_MODE +
9295
ClimateEntityFeature.FAN_MODE +
9396
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,

src/data/climate.ts

+3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ export type ClimateEntity = HassEntityBase & {
6161
preset_modes?: string[];
6262
swing_mode?: string;
6363
swing_modes?: string[];
64+
swing_horizontal_mode?: string;
65+
swing_horizontal_modes?: string[];
6466
aux_heat?: "on" | "off";
6567
};
6668
};
@@ -75,6 +77,7 @@ export const enum ClimateEntityFeature {
7577
AUX_HEAT = 64,
7678
TURN_OFF = 128,
7779
TURN_ON = 256,
80+
SWING_HORIZONTAL_MODE = 512,
7881
}
7982

8083
const hvacModeOrdering = HVAC_MODES.reduce(

src/dialogs/more-info/controls/more-info-climate.ts

+67
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ class MoreInfoClimate extends LitElement {
7474
stateObj,
7575
ClimateEntityFeature.SWING_MODE
7676
);
77+
const supportSwingHorizontalMode = supportsFeature(
78+
stateObj,
79+
ClimateEntityFeature.SWING_HORIZONTAL_MODE
80+
);
7781

7882
const currentTemperature = this.stateObj.attributes.current_temperature;
7983
const currentHumidity = this.stateObj.attributes.current_humidity;
@@ -344,6 +348,59 @@ class MoreInfoClimate extends LitElement {
344348
</ha-control-select-menu>
345349
`
346350
: nothing}
351+
${supportSwingHorizontalMode &&
352+
stateObj.attributes.swing_horizontal_modes
353+
? html`
354+
<ha-control-select-menu
355+
.label=${this.hass.formatEntityAttributeName(
356+
stateObj,
357+
"swing_horizontal_mode"
358+
)}
359+
.value=${stateObj.attributes.swing_horizontal_mode}
360+
.disabled=${this.stateObj.state === UNAVAILABLE}
361+
fixedMenuPosition
362+
naturalMenuWidth
363+
@selected=${this._handleSwingHorizontalmodeChanged}
364+
@closed=${stopPropagation}
365+
>
366+
${stateObj.attributes.swing_horizontal_mode
367+
? html`
368+
<ha-attribute-icon
369+
slot="icon"
370+
.hass=${this.hass}
371+
.stateObj=${stateObj}
372+
attribute="swing_horizontal_mode"
373+
.attributeValue=${stateObj.attributes
374+
.swing_horizontal_mode}
375+
></ha-attribute-icon>
376+
`
377+
: html`
378+
<ha-svg-icon
379+
slot="icon"
380+
.path=${mdiArrowOscillating}
381+
></ha-svg-icon>
382+
`}
383+
${stateObj.attributes.swing_horizontal_modes!.map(
384+
(mode) => html`
385+
<ha-list-item .value=${mode} graphic="icon">
386+
<ha-attribute-icon
387+
slot="graphic"
388+
.hass=${this.hass}
389+
.stateObj=${stateObj}
390+
attribute="swing_horizontal_mode"
391+
.attributeValue=${mode}
392+
></ha-attribute-icon>
393+
${this.hass.formatEntityAttributeValue(
394+
stateObj,
395+
"swing_horizontal_mode",
396+
mode
397+
)}
398+
</ha-list-item>
399+
`
400+
)}
401+
</ha-control-select-menu>
402+
`
403+
: nothing}
347404
</ha-more-info-control-select-container>
348405
`;
349406
}
@@ -380,6 +437,16 @@ class MoreInfoClimate extends LitElement {
380437
);
381438
}
382439

440+
private _handleSwingHorizontalmodeChanged(ev) {
441+
const newVal = ev.target.value;
442+
this._callServiceHelper(
443+
this.stateObj!.attributes.swing_horizontal_mode,
444+
newVal,
445+
"set_swing_horizontal_mode",
446+
{ swing_horizontal_mode: newVal }
447+
);
448+
}
449+
383450
private _handlePresetmodeChanged(ev) {
384451
const newVal = ev.target.value || null;
385452
if (newVal) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { mdiArrowOscillating } from "@mdi/js";
2+
import type { HassEntity } from "home-assistant-js-websocket";
3+
import type { PropertyValues, TemplateResult } from "lit";
4+
import { html, LitElement } from "lit";
5+
import { customElement, property, query, state } from "lit/decorators";
6+
import { stopPropagation } from "../../../common/dom/stop_propagation";
7+
import { computeDomain } from "../../../common/entity/compute_domain";
8+
import { supportsFeature } from "../../../common/entity/supports-feature";
9+
import "../../../components/ha-attribute-icon";
10+
import "../../../components/ha-control-select";
11+
import type { ControlSelectOption } from "../../../components/ha-control-select";
12+
import "../../../components/ha-control-select-menu";
13+
import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu";
14+
import type { ClimateEntity } from "../../../data/climate";
15+
import { ClimateEntityFeature } from "../../../data/climate";
16+
import { UNAVAILABLE } from "../../../data/entity";
17+
import type { HomeAssistant } from "../../../types";
18+
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
19+
import { cardFeatureStyles } from "./common/card-feature-styles";
20+
import { filterModes } from "./common/filter-modes";
21+
import type { ClimateSwingHorizontalModesCardFeatureConfig } from "./types";
22+
23+
export const supportsClimateSwingHorizontalModesCardFeature = (
24+
stateObj: HassEntity
25+
) => {
26+
const domain = computeDomain(stateObj.entity_id);
27+
return (
28+
domain === "climate" &&
29+
supportsFeature(stateObj, ClimateEntityFeature.SWING_HORIZONTAL_MODE)
30+
);
31+
};
32+
33+
@customElement("hui-climate-swing-horizontal-modes-card-feature")
34+
class HuiClimateSwingHorizontalModesCardFeature
35+
extends LitElement
36+
implements LovelaceCardFeature
37+
{
38+
@property({ attribute: false }) public hass?: HomeAssistant;
39+
40+
@property({ attribute: false }) public stateObj?: ClimateEntity;
41+
42+
@state() private _config?: ClimateSwingHorizontalModesCardFeatureConfig;
43+
44+
@state() _currentSwingHorizontalMode?: string;
45+
46+
@query("ha-control-select-menu", true)
47+
private _haSelect?: HaControlSelectMenu;
48+
49+
static getStubConfig(): ClimateSwingHorizontalModesCardFeatureConfig {
50+
return {
51+
type: "climate-swing-horizontal-modes",
52+
style: "dropdown",
53+
};
54+
}
55+
56+
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
57+
await import(
58+
"../editor/config-elements/hui-climate-swing-horizontal-modes-card-feature-editor"
59+
);
60+
return document.createElement(
61+
"hui-climate-swing-horizontal-modes-card-feature-editor"
62+
);
63+
}
64+
65+
public setConfig(config: ClimateSwingHorizontalModesCardFeatureConfig): void {
66+
if (!config) {
67+
throw new Error("Invalid configuration");
68+
}
69+
this._config = config;
70+
}
71+
72+
protected willUpdate(changedProp: PropertyValues): void {
73+
super.willUpdate(changedProp);
74+
if (changedProp.has("stateObj") && this.stateObj) {
75+
this._currentSwingHorizontalMode =
76+
this.stateObj.attributes.swing_horizontal_mode;
77+
}
78+
}
79+
80+
protected updated(changedProps: PropertyValues) {
81+
super.updated(changedProps);
82+
if (this._haSelect && changedProps.has("hass")) {
83+
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
84+
if (
85+
this.hass &&
86+
this.hass.formatEntityAttributeValue !==
87+
oldHass?.formatEntityAttributeValue
88+
) {
89+
this._haSelect.layoutOptions();
90+
}
91+
}
92+
}
93+
94+
private async _valueChanged(ev: CustomEvent) {
95+
const swingHorizontalMode =
96+
(ev.detail as any).value ?? ((ev.target as any).value as string);
97+
98+
const oldSwingHorizontalMode =
99+
this.stateObj!.attributes.swing_horizontal_mode;
100+
101+
if (swingHorizontalMode === oldSwingHorizontalMode) return;
102+
103+
this._currentSwingHorizontalMode = swingHorizontalMode;
104+
105+
try {
106+
await this._setMode(swingHorizontalMode);
107+
} catch (err) {
108+
this._currentSwingHorizontalMode = oldSwingHorizontalMode;
109+
}
110+
}
111+
112+
private async _setMode(mode: string) {
113+
await this.hass!.callService("climate", "set_swing_horizontal_mode", {
114+
entity_id: this.stateObj!.entity_id,
115+
swing_horizontal_mode: mode,
116+
});
117+
}
118+
119+
protected render(): TemplateResult | null {
120+
if (
121+
!this._config ||
122+
!this.hass ||
123+
!this.stateObj ||
124+
!supportsClimateSwingHorizontalModesCardFeature(this.stateObj)
125+
) {
126+
return null;
127+
}
128+
129+
const stateObj = this.stateObj;
130+
131+
const options = filterModes(
132+
stateObj.attributes.swing_horizontal_modes,
133+
this._config!.swing_horizontal_modes
134+
).map<ControlSelectOption>((mode) => ({
135+
value: mode,
136+
label: this.hass!.formatEntityAttributeValue(
137+
this.stateObj!,
138+
"swing_horizontal_mode",
139+
mode
140+
),
141+
icon: html`<ha-attribute-icon
142+
slot="graphic"
143+
.hass=${this.hass}
144+
.stateObj=${stateObj}
145+
attribute="swing_horizontal_mode"
146+
.attributeValue=${mode}
147+
></ha-attribute-icon>`,
148+
}));
149+
150+
if (this._config.style === "icons") {
151+
return html`
152+
<ha-control-select
153+
.options=${options}
154+
.value=${this._currentSwingHorizontalMode}
155+
@value-changed=${this._valueChanged}
156+
hide-label
157+
.ariaLabel=${this.hass!.formatEntityAttributeName(
158+
stateObj,
159+
"swing_horizontal_mode"
160+
)}
161+
.disabled=${this.stateObj!.state === UNAVAILABLE}
162+
>
163+
</ha-control-select>
164+
`;
165+
}
166+
167+
return html`
168+
<ha-control-select-menu
169+
show-arrow
170+
hide-label
171+
.label=${this.hass!.formatEntityAttributeName(
172+
stateObj,
173+
"swing_horizontal_mode"
174+
)}
175+
.value=${this._currentSwingHorizontalMode}
176+
.disabled=${this.stateObj.state === UNAVAILABLE}
177+
fixedMenuPosition
178+
naturalMenuWidth
179+
@selected=${this._valueChanged}
180+
@closed=${stopPropagation}
181+
>
182+
${this._currentSwingHorizontalMode
183+
? html`<ha-attribute-icon
184+
slot="icon"
185+
.hass=${this.hass}
186+
.stateObj=${stateObj}
187+
attribute="swing_horizontal_mode"
188+
.attributeValue=${this._currentSwingHorizontalMode}
189+
></ha-attribute-icon>`
190+
: html` <ha-svg-icon
191+
slot="icon"
192+
.path=${mdiArrowOscillating}
193+
></ha-svg-icon>`}
194+
${options.map(
195+
(option) => html`
196+
<ha-list-item .value=${option.value} graphic="icon">
197+
${option.icon}${option.label}
198+
</ha-list-item>
199+
`
200+
)}
201+
</ha-control-select-menu>
202+
`;
203+
}
204+
205+
static get styles() {
206+
return cardFeatureStyles;
207+
}
208+
}
209+
210+
declare global {
211+
interface HTMLElementTagNameMap {
212+
"hui-climate-swing-horizontal-modes-card-feature": HuiClimateSwingHorizontalModesCardFeature;
213+
}
214+
}

src/panels/lovelace/card-features/types.ts

+7
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ export interface ClimateSwingModesCardFeatureConfig {
6161
swing_modes?: string[];
6262
}
6363

64+
export interface ClimateSwingHorizontalModesCardFeatureConfig {
65+
type: "climate-swing-horizontal-modes";
66+
style?: "dropdown" | "icons";
67+
swing_horizontal_modes?: string[];
68+
}
69+
6470
export interface ClimateHvacModesCardFeatureConfig {
6571
type: "climate-hvac-modes";
6672
style?: "dropdown" | "icons";
@@ -139,6 +145,7 @@ export type LovelaceCardFeatureConfig =
139145
| AlarmModesCardFeatureConfig
140146
| ClimateFanModesCardFeatureConfig
141147
| ClimateSwingModesCardFeatureConfig
148+
| ClimateSwingHorizontalModesCardFeatureConfig
142149
| ClimateHvacModesCardFeatureConfig
143150
| ClimatePresetModesCardFeatureConfig
144151
| CoverOpenCloseCardFeatureConfig

0 commit comments

Comments
 (0)