-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Expand file tree
/
Copy pathfan.py
More file actions
343 lines (307 loc) · 13.6 KB
/
fan.py
File metadata and controls
343 lines (307 loc) · 13.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
"""Matter Fan platform support."""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from chip.clusters import Objects as clusters
from homeassistant.components.fan import (
DIRECTION_FORWARD,
DIRECTION_REVERSE,
FanEntity,
FanEntityDescription,
FanEntityFeature,
)
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .entity import MatterEntity, MatterEntityDescription
from .helpers import MatterConfigEntry
from .models import MatterDiscoverySchema
FanControlFeature = clusters.FanControl.Bitmaps.Feature
WindBitmap = clusters.FanControl.Bitmaps.WindBitmap
FanModeSequenceEnum = clusters.FanControl.Enums.FanModeSequenceEnum
PRESET_LOW = "low"
PRESET_MEDIUM = "medium"
PRESET_HIGH = "high"
PRESET_AUTO = "auto"
FAN_MODE_MAP = {
PRESET_LOW: clusters.FanControl.Enums.FanModeEnum.kLow,
PRESET_MEDIUM: clusters.FanControl.Enums.FanModeEnum.kMedium,
PRESET_HIGH: clusters.FanControl.Enums.FanModeEnum.kHigh,
PRESET_AUTO: clusters.FanControl.Enums.FanModeEnum.kAuto,
}
FAN_MODE_MAP_REVERSE = {v: k for k, v in FAN_MODE_MAP.items()}
# special preset modes for wind feature
PRESET_NATURAL_WIND = "natural_wind"
PRESET_SLEEP_WIND = "sleep_wind"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: MatterConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Matter fan from Config Entry."""
matter = config_entry.runtime_data.adapter
matter.register_platform_handler(Platform.FAN, async_add_entities)
@dataclass(frozen=True, kw_only=True)
class MatterFanEntityDescription(FanEntityDescription, MatterEntityDescription):
"""Describe Matter Fan entities."""
class MatterFan(MatterEntity, FanEntity):
"""Representation of a Matter fan."""
_last_known_preset_mode: str | None = None
_last_known_percentage: int = 0
_feature_map: int | None = None
_platform_translation_key = "fan"
async def async_turn_on(
self,
percentage: int | None = None,
preset_mode: str | None = None,
**kwargs: Any,
) -> None:
"""Turn on the fan."""
if percentage is None and preset_mode is None:
# turn_on without explicit percentage or preset_mode given
# try to handle this with the last known value
if self._last_known_percentage != 0:
percentage = self._last_known_percentage
elif self._last_known_preset_mode is not None:
preset_mode = self._last_known_preset_mode
elif self._attr_preset_modes:
# fallback: default to first supported preset
preset_mode = self._attr_preset_modes[0]
else:
# this really should not be possible but handle it anyways
percentage = 50
# prefer setting fan speed by percentage
if percentage is not None:
await self.async_set_percentage(percentage)
return
# handle setting fan mode by preset
if TYPE_CHECKING:
assert preset_mode is not None
await self.async_set_preset_mode(preset_mode)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn fan off."""
# clear the wind setting if its currently set
if self._attr_preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]:
await self._set_wind_mode(None)
await self.write_attribute(
value=clusters.FanControl.Enums.FanModeEnum.kOff,
matter_attribute=clusters.FanControl.Attributes.FanMode,
)
async def async_set_percentage(self, percentage: int) -> None:
"""Set the speed of the fan, as a percentage."""
await self.write_attribute(
value=percentage,
matter_attribute=clusters.FanControl.Attributes.PercentSetting,
)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
# handle wind as preset
if preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]:
await self._set_wind_mode(preset_mode)
return
# clear the wind setting if its currently set
if self._attr_preset_mode in [PRESET_NATURAL_WIND, PRESET_SLEEP_WIND]:
await self._set_wind_mode(None)
await self.write_attribute(
value=FAN_MODE_MAP[preset_mode],
matter_attribute=clusters.FanControl.Attributes.FanMode,
)
async def async_oscillate(self, oscillating: bool) -> None:
"""Oscillate the fan."""
await self.write_attribute(
value=(
self.get_matter_attribute_value(
clusters.FanControl.Attributes.RockSupport
)
if oscillating
else 0
),
matter_attribute=clusters.FanControl.Attributes.RockSetting,
)
async def async_set_direction(self, direction: str) -> None:
"""Set the direction of the fan."""
await self.write_attribute(
value=(
clusters.FanControl.Enums.AirflowDirectionEnum.kReverse
if direction == DIRECTION_REVERSE
else clusters.FanControl.Enums.AirflowDirectionEnum.kForward
),
matter_attribute=clusters.FanControl.Attributes.AirflowDirection,
)
async def _set_wind_mode(self, wind_mode: str | None) -> None:
"""Set wind mode."""
if wind_mode == PRESET_NATURAL_WIND:
wind_setting = WindBitmap.kNaturalWind
elif wind_mode == PRESET_SLEEP_WIND:
wind_setting = WindBitmap.kSleepWind
else:
wind_setting = 0
await self.write_attribute(
value=wind_setting,
matter_attribute=clusters.FanControl.Attributes.WindSetting,
)
@callback
def _update_from_device(self) -> None:
"""Update from device."""
self._calculate_features()
if self.get_matter_attribute_value(clusters.OnOff.Attributes.OnOff) is False:
# special case: the appliance has a dedicated Power switch on the OnOff cluster
# if the mains power is off - treat it as if the fan mode is off
self._attr_preset_mode = None
self._attr_percentage = 0
return
if self._attr_supported_features & FanEntityFeature.DIRECTION:
direction_value = self.get_matter_attribute_value(
clusters.FanControl.Attributes.AirflowDirection
)
self._attr_current_direction = (
DIRECTION_REVERSE
if direction_value
== clusters.FanControl.Enums.AirflowDirectionEnum.kReverse
else DIRECTION_FORWARD
)
if self._attr_supported_features & FanEntityFeature.OSCILLATE:
self._attr_oscillating = (
self.get_matter_attribute_value(
clusters.FanControl.Attributes.RockSetting
)
!= 0
)
# speed percentage is always provided
current_percent = self.get_matter_attribute_value(
clusters.FanControl.Attributes.PercentCurrent
)
# NOTE that a device may give back 255 as a special value to indicate that
# the speed is under automatic control and not set to a specific value.
self._attr_percentage = None if current_percent == 255 else current_percent
# get preset mode from fan mode (and wind feature if available)
wind_setting = self.get_matter_attribute_value(
clusters.FanControl.Attributes.WindSetting
)
fan_mode = self.get_matter_attribute_value(
clusters.FanControl.Attributes.FanMode
)
if fan_mode == clusters.FanControl.Enums.FanModeEnum.kOff:
self._attr_preset_mode = None
self._attr_percentage = 0
elif (
self._attr_preset_modes
and PRESET_NATURAL_WIND in self._attr_preset_modes
and wind_setting & WindBitmap.kNaturalWind
):
self._attr_preset_mode = PRESET_NATURAL_WIND
elif (
self._attr_preset_modes
and PRESET_SLEEP_WIND in self._attr_preset_modes
and wind_setting & WindBitmap.kSleepWind
):
self._attr_preset_mode = PRESET_SLEEP_WIND
else:
fan_mode = self.get_matter_attribute_value(
clusters.FanControl.Attributes.FanMode
)
self._attr_preset_mode = FAN_MODE_MAP_REVERSE.get(fan_mode)
# keep track of the last known mode for turn_on commands without preset
if self._attr_preset_mode is not None:
self._last_known_preset_mode = self._attr_preset_mode
if current_percent:
self._last_known_percentage = current_percent
@callback
def _calculate_features(
self,
) -> None:
"""Calculate features for HA Fan platform from Matter FeatureMap."""
feature_map = int(
self.get_matter_attribute_value(clusters.FanControl.Attributes.FeatureMap)
)
# NOTE: the featuremap can dynamically change, so we need to update the
# supported features if the featuremap changes.
# work out supported features and presets from matter featuremap
if self._feature_map == feature_map:
return
self._feature_map = feature_map
self._attr_supported_features = FanEntityFeature(0)
# PercentSetting is always a mandatory attribute of the FanControl cluster,
# so percentage-based speed control is always available.
self._attr_supported_features |= FanEntityFeature.SET_SPEED
# Reset to default so a featuremap change from MultiSpeed -> non-MultiSpeed
# does not leave a stale speed_count / percentage_step.
self._attr_speed_count = 100
if feature_map & FanControlFeature.kMultiSpeed:
self._attr_speed_count = int(
self.get_matter_attribute_value(clusters.FanControl.Attributes.SpeedMax)
)
if feature_map & FanControlFeature.kRocking:
# NOTE: the Matter model allows that a device can have multiple/different
# rock directions while HA doesn't allow this in the entity model.
# For now we just assume that a device has a single rock direction and the
# Matter spec is just future proofing for devices that might have multiple
# rock directions. As soon as devices show up that actually support multiple
# directions, we need to either update the HA Fan entity model or maybe add
# this as a separate entity.
self._attr_supported_features |= FanEntityFeature.OSCILLATE
# figure out supported preset modes
preset_modes = []
fan_mode_seq = int(
self.get_matter_attribute_value(
clusters.FanControl.Attributes.FanModeSequence
)
)
if fan_mode_seq == FanModeSequenceEnum.kOffLowHigh:
preset_modes = [PRESET_LOW, PRESET_HIGH]
elif fan_mode_seq == FanModeSequenceEnum.kOffLowHighAuto:
preset_modes = [PRESET_LOW, PRESET_HIGH, PRESET_AUTO]
elif fan_mode_seq == FanModeSequenceEnum.kOffLowMedHigh:
preset_modes = [PRESET_LOW, PRESET_MEDIUM, PRESET_HIGH]
elif fan_mode_seq == FanModeSequenceEnum.kOffLowMedHighAuto:
preset_modes = [PRESET_LOW, PRESET_MEDIUM, PRESET_HIGH, PRESET_AUTO]
elif fan_mode_seq == FanModeSequenceEnum.kOffHighAuto:
preset_modes = [PRESET_HIGH, PRESET_AUTO]
elif fan_mode_seq == FanModeSequenceEnum.kOffHigh:
preset_modes = [PRESET_HIGH]
# treat Matter Wind feature as additional preset(s)
if feature_map & FanControlFeature.kWind:
wind_support = int(
self.get_matter_attribute_value(
clusters.FanControl.Attributes.WindSupport
)
)
if wind_support & WindBitmap.kNaturalWind:
preset_modes.append(PRESET_NATURAL_WIND)
if wind_support & WindBitmap.kSleepWind:
preset_modes.append(PRESET_SLEEP_WIND)
if len(preset_modes) > 0:
self._attr_supported_features |= FanEntityFeature.PRESET_MODE
self._attr_preset_modes = preset_modes
if feature_map & FanControlFeature.kAirflowDirection:
self._attr_supported_features |= FanEntityFeature.DIRECTION
self._attr_supported_features |= (
FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
)
# Discovery schema(s) to map Matter Attributes to HA entities
DISCOVERY_SCHEMAS = [
MatterDiscoverySchema(
platform=Platform.FAN,
entity_description=MatterFanEntityDescription(
key="MatterFan",
name=None,
),
entity_class=MatterFan,
# FanEntityFeature
required_attributes=(
clusters.FanControl.Attributes.FanMode,
clusters.FanControl.Attributes.PercentCurrent,
clusters.FanControl.Attributes.PercentSetting,
),
# PercentSetting SHALL be null when FanMode is Auto (spec 4.4.6.3),
# so allow null values to not block discovery in that state.
allow_none_value=True,
optional_attributes=(
clusters.FanControl.Attributes.SpeedSetting,
clusters.FanControl.Attributes.RockSetting,
clusters.FanControl.Attributes.WindSetting,
clusters.FanControl.Attributes.AirflowDirection,
clusters.OnOff.Attributes.OnOff,
),
),
]