Skip to content

Commit 9281204

Browse files
pcan08timmo001MindFreeze
authored
Add fan oscillate feature (home-assistant#26519)
* First working fan-oscillate feature This a first working impl, need at least to do: - Tooltip not yet "Yes/No" - Need implementation verification * Use same strings as more info label for control tooltip * Add missing label for editor * Rename some variables * Add fan features in gallery * Fix lint:types by applying suggestions from code review Co-authored-by: Aidan Timson <[email protected]> * fix lint new line after import * fix typo Co-authored-by: Petar Petrov <[email protected]> * fix event value type treating * remove type magic as suggested Co-authored-by: Petar Petrov <[email protected]> * Update localize.ts Complete suggestion change to have tooltip * fix lint by removing unused import --------- Co-authored-by: Aidan Timson <[email protected]> Co-authored-by: Petar Petrov <[email protected]>
1 parent 9fc14d6 commit 9281204

File tree

10 files changed

+271
-3
lines changed

10 files changed

+271
-3
lines changed

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

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,14 @@ const ENTITIES = [
101101
ClimateEntityFeature.FAN_MODE +
102102
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
103103
}),
104-
getEntity("fan", "fan_direction", "on", {
104+
getEntity("fan", "fan_demo", "on", {
105105
friendly_name: "Ceiling fan",
106106
device_class: "fan",
107107
direction: "reverse",
108-
supported_features: [FanEntityFeature.DIRECTION],
108+
supported_features:
109+
FanEntityFeature.DIRECTION +
110+
FanEntityFeature.SET_SPEED +
111+
FanEntityFeature.OSCILLATE,
109112
}),
110113
];
111114

@@ -272,11 +275,29 @@ const CONFIGS = [
272275
heading: "Fan direction feature",
273276
config: `
274277
- type: tile
275-
entity: fan.fan_direction
278+
entity: fan.fan_demo
276279
features:
277280
- type: fan-direction
278281
`,
279282
},
283+
{
284+
heading: "Fan speed feature",
285+
config: `
286+
- type: tile
287+
entity: fan.fan_demo
288+
features:
289+
- type: fan-speed
290+
`,
291+
},
292+
{
293+
heading: "Fan oscillate feature",
294+
config: `
295+
- type: tile
296+
entity: fan.fan_demo
297+
features:
298+
- type: fan-oscillate
299+
`,
300+
},
280301
];
281302

282303
@customElement("demo-lovelace-tile-card")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
---
2+
title: Fan
3+
---

gallery/src/pages/more-info/fan.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { PropertyValues, TemplateResult } from "lit";
2+
import { html, LitElement } from "lit";
3+
import { customElement, property, query } from "lit/decorators";
4+
import "../../../../src/components/ha-card";
5+
import "../../../../src/dialogs/more-info/more-info-content";
6+
import { getEntity } from "../../../../src/fake_data/entity";
7+
import type { MockHomeAssistant } from "../../../../src/fake_data/provide_hass";
8+
import { provideHass } from "../../../../src/fake_data/provide_hass";
9+
import "../../components/demo-more-infos";
10+
import { FanEntityFeature } from "../../../../src/data/fan";
11+
12+
const ENTITIES = [
13+
getEntity("fan", "fan", "on", {
14+
friendly_name: "Fan",
15+
device_class: "fan",
16+
supported_features:
17+
FanEntityFeature.OSCILLATE +
18+
FanEntityFeature.DIRECTION +
19+
FanEntityFeature.SET_SPEED,
20+
}),
21+
];
22+
23+
@customElement("demo-more-info-fan")
24+
class DemoMoreInfoFan extends LitElement {
25+
@property({ attribute: false }) public hass!: MockHomeAssistant;
26+
27+
@query("demo-more-infos") private _demoRoot!: HTMLElement;
28+
29+
protected render(): TemplateResult {
30+
return html`
31+
<demo-more-infos
32+
.hass=${this.hass}
33+
.entities=${ENTITIES.map((ent) => ent.entityId)}
34+
></demo-more-infos>
35+
`;
36+
}
37+
38+
protected firstUpdated(changedProperties: PropertyValues) {
39+
super.firstUpdated(changedProperties);
40+
const hass = provideHass(this._demoRoot);
41+
hass.updateTranslations(null, "en");
42+
hass.addEntities(ENTITIES);
43+
}
44+
}
45+
46+
declare global {
47+
interface HTMLElementTagNameMap {
48+
"demo-more-info-fan": DemoMoreInfoFan;
49+
}
50+
}

src/common/translations/localize.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export type LocalizeKeys =
1414
| `ui.card.weather.attributes.${string}`
1515
| `ui.card.weather.cardinal_direction.${string}`
1616
| `ui.card.lawn_mower.actions.${string}`
17+
| `ui.common.${string}`
1718
| `ui.components.calendar.event.rrule.${string}`
1819
| `ui.components.selectors.file.${string}`
1920
| `ui.components.logbook.messages.detected_device_classes.${string}`

src/fake_data/entity.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,35 @@ class WaterHeaterEntity extends Entity {
441441
}
442442
}
443443

444+
class FanEntity extends Entity {
445+
static CAPABILITY_ATTRIBUTES = new Set([
446+
...CAPABILITY_ATTRIBUTES,
447+
"direction",
448+
"oscillating",
449+
"percentage",
450+
]);
451+
452+
public async handleService(domain, service, data) {
453+
if (domain !== this.domain) {
454+
return;
455+
}
456+
457+
if (["turn_on", "turn_off"].includes(service)) {
458+
this.update(service === "turn_on" ? "on" : "off");
459+
} else if (
460+
["set_direction", "oscillate", "set_percentage"].includes(service)
461+
) {
462+
const { entity_id, ...toSet } = data;
463+
this.update(this.state, {
464+
...this.attributes,
465+
...toSet,
466+
});
467+
} else {
468+
super.handleService(domain, service, data);
469+
}
470+
}
471+
}
472+
444473
class GroupEntity extends Entity {
445474
public async handleService(domain, service, data) {
446475
if (!["homeassistant", this.domain].includes(domain)) {
@@ -463,6 +492,7 @@ const TYPES = {
463492
alarm_control_panel: AlarmControlPanelEntity,
464493
climate: ClimateEntity,
465494
cover: CoverEntity,
495+
fan: FanEntity,
466496
group: GroupEntity,
467497
input_boolean: ToggleEntity,
468498
input_number: InputNumberEntity,
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { mdiArrowOscillating, mdiArrowOscillatingOff } from "@mdi/js";
2+
import type { PropertyValues, TemplateResult } from "lit";
3+
import { LitElement, html } from "lit";
4+
import { customElement, property, state } from "lit/decorators";
5+
import { styleMap } from "lit/directives/style-map";
6+
import { computeDomain } from "../../../common/entity/compute_domain";
7+
import { stateColorCss } from "../../../common/entity/state_color";
8+
import "../../../components/ha-control-select";
9+
import type { ControlSelectOption } from "../../../components/ha-control-select";
10+
import { UNAVAILABLE } from "../../../data/entity";
11+
import type { FanEntity } from "../../../data/fan";
12+
import { FanEntityFeature } from "../../../data/fan";
13+
import type { HomeAssistant } from "../../../types";
14+
import type { LovelaceCardFeature } from "../types";
15+
import { cardFeatureStyles } from "./common/card-feature-styles";
16+
import type {
17+
FanOscillateCardFeatureConfig,
18+
LovelaceCardFeatureContext,
19+
} from "./types";
20+
import { supportsFeature } from "../../../common/entity/supports-feature";
21+
22+
export const supportsFanOscilatteCardFeature = (
23+
hass: HomeAssistant,
24+
context: LovelaceCardFeatureContext
25+
) => {
26+
const stateObj = context.entity_id
27+
? hass.states[context.entity_id]
28+
: undefined;
29+
if (!stateObj) return false;
30+
const domain = computeDomain(stateObj.entity_id);
31+
return (
32+
domain === "fan" && supportsFeature(stateObj, FanEntityFeature.OSCILLATE)
33+
);
34+
};
35+
36+
@customElement("hui-fan-oscillate-card-feature")
37+
class HuiFanOscillateCardFeature
38+
extends LitElement
39+
implements LovelaceCardFeature
40+
{
41+
@property({ attribute: false }) public hass?: HomeAssistant;
42+
43+
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
44+
45+
@state() private _config?: FanOscillateCardFeatureConfig;
46+
47+
@state() _oscillate?: boolean;
48+
49+
private get _stateObj() {
50+
if (!this.hass || !this.context || !this.context.entity_id) {
51+
return undefined;
52+
}
53+
return this.hass.states[this.context.entity_id!] as FanEntity | undefined;
54+
}
55+
56+
static getStubConfig(): FanOscillateCardFeatureConfig {
57+
return {
58+
type: "fan-oscillate",
59+
};
60+
}
61+
62+
public setConfig(config: FanOscillateCardFeatureConfig): void {
63+
if (!config) {
64+
throw new Error("Invalid configuration");
65+
}
66+
this._config = config;
67+
}
68+
69+
protected willUpdate(changedProp: PropertyValues): void {
70+
if (
71+
(changedProp.has("hass") || changedProp.has("context")) &&
72+
this._stateObj
73+
) {
74+
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
75+
const oldStateObj = oldHass?.states[this.context!.entity_id!];
76+
if (oldStateObj !== this._stateObj) {
77+
this._oscillate = this._stateObj.attributes.oscillating;
78+
}
79+
}
80+
}
81+
82+
private async _valueChanged(ev: CustomEvent) {
83+
const shouldOscillate = (ev.detail as any).value === "yes";
84+
85+
if (shouldOscillate === this._stateObj!.attributes.oscillating) return;
86+
87+
const wasOscillating = this._stateObj!.attributes.oscillating;
88+
this._oscillate = shouldOscillate;
89+
90+
try {
91+
await this._updateOscillate(shouldOscillate);
92+
} catch (_err) {
93+
this._oscillate = wasOscillating;
94+
}
95+
}
96+
97+
private async _updateOscillate(oscillate: boolean) {
98+
await this.hass!.callService("fan", "oscillate", {
99+
entity_id: this._stateObj!.entity_id,
100+
oscillating: oscillate,
101+
});
102+
}
103+
104+
protected render(): TemplateResult | null {
105+
if (
106+
!this._config ||
107+
!this.hass ||
108+
!this.context ||
109+
!this._stateObj ||
110+
!supportsFanOscilatteCardFeature(this.hass, this.context)
111+
) {
112+
return null;
113+
}
114+
115+
const color = stateColorCss(this._stateObj);
116+
117+
const yesNo = ["no", "yes"] as const;
118+
const options = yesNo.map<ControlSelectOption>((oscillating) => ({
119+
value: oscillating,
120+
label: this.hass!.localize(`ui.common.${oscillating}`),
121+
path:
122+
oscillating === "yes" ? mdiArrowOscillating : mdiArrowOscillatingOff,
123+
}));
124+
125+
return html`
126+
<ha-control-select
127+
.options=${options}
128+
.value=${this._oscillate ? "yes" : "no"}
129+
@value-changed=${this._valueChanged}
130+
hide-option-label
131+
.label=${this.hass.localize("ui.card.fan.oscillate")}
132+
style=${styleMap({
133+
"--control-select-color": color,
134+
})}
135+
.disabled=${this._stateObj!.state === UNAVAILABLE}
136+
>
137+
</ha-control-select>
138+
`;
139+
}
140+
141+
static get styles() {
142+
return cardFeatureStyles;
143+
}
144+
}
145+
146+
declare global {
147+
interface HTMLElementTagNameMap {
148+
"hui-fan-oscillate-card-feature": HuiFanOscillateCardFeature;
149+
}
150+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export interface FanDirectionCardFeatureConfig {
4747
type: "fan-direction";
4848
}
4949

50+
export interface FanOscillateCardFeatureConfig {
51+
type: "fan-oscillate";
52+
}
53+
5054
export interface FanPresetModesCardFeatureConfig {
5155
type: "fan-preset-modes";
5256
style?: "dropdown" | "icons";
@@ -219,6 +223,7 @@ export type LovelaceCardFeatureConfig =
219223
| CoverTiltCardFeatureConfig
220224
| DateSetCardFeatureConfig
221225
| FanDirectionCardFeatureConfig
226+
| FanOscillateCardFeatureConfig
222227
| FanPresetModesCardFeatureConfig
223228
| FanSpeedCardFeatureConfig
224229
| HumidifierToggleCardFeatureConfig

src/panels/lovelace/create-element/create-card-feature-element.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import "../card-features/hui-cover-tilt-card-feature";
1212
import "../card-features/hui-cover-tilt-position-card-feature";
1313
import "../card-features/hui-date-set-card-feature";
1414
import "../card-features/hui-fan-direction-card-feature";
15+
import "../card-features/hui-fan-oscillate-card-feature";
1516
import "../card-features/hui-fan-preset-modes-card-feature";
1617
import "../card-features/hui-fan-speed-card-feature";
1718
import "../card-features/hui-humidifier-modes-card-feature";
@@ -56,6 +57,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
5657
"cover-tilt",
5758
"date-set",
5859
"fan-direction",
60+
"fan-oscillate",
5961
"fan-preset-modes",
6062
"fan-speed",
6163
"humidifier-modes",

src/panels/lovelace/editor/config-elements/hui-card-features-editor.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt
3232
import { supportsCoverTiltPositionCardFeature } from "../../card-features/hui-cover-tilt-position-card-feature";
3333
import { supportsDateSetCardFeature } from "../../card-features/hui-date-set-card-feature";
3434
import { supportsFanDirectionCardFeature } from "../../card-features/hui-fan-direction-card-feature";
35+
import { supportsFanOscilatteCardFeature } from "../../card-features/hui-fan-oscillate-card-feature";
3536
import { supportsFanPresetModesCardFeature } from "../../card-features/hui-fan-preset-modes-card-feature";
3637
import { supportsFanSpeedCardFeature } from "../../card-features/hui-fan-speed-card-feature";
3738
import { supportsHumidifierModesCardFeature } from "../../card-features/hui-humidifier-modes-card-feature";
@@ -81,6 +82,7 @@ const UI_FEATURE_TYPES = [
8182
"cover-tilt",
8283
"date-set",
8384
"fan-direction",
85+
"fan-oscillate",
8486
"fan-preset-modes",
8587
"fan-speed",
8688
"humidifier-modes",
@@ -145,6 +147,7 @@ const SUPPORTS_FEATURE_TYPES: Record<
145147
"cover-tilt": supportsCoverTiltCardFeature,
146148
"date-set": supportsDateSetCardFeature,
147149
"fan-direction": supportsFanDirectionCardFeature,
150+
"fan-oscillate": supportsFanOscilatteCardFeature,
148151
"fan-preset-modes": supportsFanPresetModesCardFeature,
149152
"fan-speed": supportsFanSpeedCardFeature,
150153
"humidifier-modes": supportsHumidifierModesCardFeature,

src/translations/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7868,6 +7868,9 @@
78687868
"date-set": {
78697869
"label": "Set date"
78707870
},
7871+
"fan-oscillate": {
7872+
"label": "Fan oscillation"
7873+
},
78717874
"fan-direction": {
78727875
"label": "Fan direction"
78737876
},

0 commit comments

Comments
 (0)