Home Assistant Unofficial Reference 2024.12.1
fan.py
Go to the documentation of this file.
1 """Support for Homekit fans."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from aiohomekit.model.characteristics import CharacteristicsTypes
8 from aiohomekit.model.services import Service, ServicesTypes
9 from propcache import cached_property
10 
11 from homeassistant.components.fan import (
12  DIRECTION_FORWARD,
13  DIRECTION_REVERSE,
14  FanEntity,
15  FanEntityFeature,
16 )
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import Platform
19 from homeassistant.core import HomeAssistant, callback
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22  percentage_to_ranged_value,
23  ranged_value_to_percentage,
24 )
25 
26 from . import KNOWN_DEVICES
27 from .connection import HKDevice
28 from .entity import HomeKitEntity
29 
30 # 0 is clockwise, 1 is counter-clockwise. The match to forward and reverse is so that
31 # its consistent with homeassistant.components.homekit.
32 DIRECTION_TO_HK = {
33  DIRECTION_REVERSE: 1,
34  DIRECTION_FORWARD: 0,
35 }
36 HK_DIRECTION_TO_HA = {v: k for (k, v) in DIRECTION_TO_HK.items()}
37 
38 
40  """Representation of a Homekit fan."""
41 
42  # This must be set in subclasses to the name of a boolean characteristic
43  # that controls whether the fan is on or off.
44  on_characteristic: str
45  _enable_turn_on_off_backwards_compatibility = False
46 
47  @callback
48  def _async_reconfigure(self) -> None:
49  """Reconfigure entity."""
50  self._async_clear_property_cache_async_clear_property_cache(
51  (
52  "_speed_range",
53  "_min_speed",
54  "_max_speed",
55  "speed_count",
56  "supported_features",
57  )
58  )
59  super()._async_reconfigure()
60 
61  def get_characteristic_types(self) -> list[str]:
62  """Define the homekit characteristics the entity cares about."""
63  return [
64  CharacteristicsTypes.SWING_MODE,
65  CharacteristicsTypes.ROTATION_DIRECTION,
66  CharacteristicsTypes.ROTATION_SPEED,
67  self.on_characteristic,
68  ]
69 
70  @property
71  def is_on(self) -> bool:
72  """Return true if device is on."""
73  return self.serviceservice.value(self.on_characteristic) == 1
74 
75  @cached_property
76  def _speed_range(self) -> tuple[int, int]:
77  """Return the speed range."""
78  return (self._min_speed_min_speed, self._max_speed_max_speed)
79 
80  @cached_property
81  def _min_speed(self) -> int:
82  """Return the minimum speed."""
83  return (
84  round(self.serviceservice[CharacteristicsTypes.ROTATION_SPEED].minValue or 0) + 1
85  )
86 
87  @cached_property
88  def _max_speed(self) -> int:
89  """Return the minimum speed."""
90  return round(self.serviceservice[CharacteristicsTypes.ROTATION_SPEED].maxValue or 100)
91 
92  @property
93  def percentage(self) -> int:
94  """Return the current speed percentage."""
95  if not self.is_onis_onis_onis_on:
96  return 0
97 
99  self._speed_range_speed_range, self.serviceservice.value(CharacteristicsTypes.ROTATION_SPEED)
100  )
101 
102  @property
103  def current_direction(self) -> str:
104  """Return the current direction of the fan."""
105  direction = self.serviceservice.value(CharacteristicsTypes.ROTATION_DIRECTION)
106  return HK_DIRECTION_TO_HA[direction]
107 
108  @property
109  def oscillating(self) -> bool:
110  """Return whether or not the fan is currently oscillating."""
111  oscillating = self.serviceservice.value(CharacteristicsTypes.SWING_MODE)
112  return oscillating == 1
113 
114  @cached_property
115  def supported_features(self) -> FanEntityFeature:
116  """Flag supported features."""
117  features = FanEntityFeature.TURN_OFF | FanEntityFeature.TURN_ON
118 
119  if self.serviceservice.has(CharacteristicsTypes.ROTATION_DIRECTION):
120  features |= FanEntityFeature.DIRECTION
121 
122  if self.serviceservice.has(CharacteristicsTypes.ROTATION_SPEED):
123  features |= FanEntityFeature.SET_SPEED
124 
125  if self.serviceservice.has(CharacteristicsTypes.SWING_MODE):
126  features |= FanEntityFeature.OSCILLATE
127 
128  return features
129 
130  @cached_property
131  def speed_count(self) -> int:
132  """Speed count for the fan."""
133  return round(
134  min(self._max_speed_max_speed, 100)
135  / max(1, self.serviceservice[CharacteristicsTypes.ROTATION_SPEED].minStep or 0)
136  )
137 
138  async def async_set_direction(self, direction: str) -> None:
139  """Set the direction of the fan."""
140  await self.async_put_characteristicsasync_put_characteristics(
141  {CharacteristicsTypes.ROTATION_DIRECTION: DIRECTION_TO_HK[direction]}
142  )
143 
144  async def async_set_percentage(self, percentage: int) -> None:
145  """Set the speed of the fan."""
146  if percentage == 0:
147  await self.async_turn_offasync_turn_offasync_turn_off()
148  return
149 
150  await self.async_put_characteristicsasync_put_characteristics(
151  {
152  CharacteristicsTypes.ROTATION_SPEED: round(
153  percentage_to_ranged_value(self._speed_range_speed_range, percentage)
154  )
155  }
156  )
157 
158  async def async_oscillate(self, oscillating: bool) -> None:
159  """Oscillate the fan."""
160  await self.async_put_characteristicsasync_put_characteristics(
161  {CharacteristicsTypes.SWING_MODE: 1 if oscillating else 0}
162  )
163 
164  async def async_turn_on(
165  self,
166  percentage: int | None = None,
167  preset_mode: str | None = None,
168  **kwargs: Any,
169  ) -> None:
170  """Turn the specified fan on."""
171  characteristics: dict[str, Any] = {}
172 
173  if not self.is_onis_onis_onis_on:
174  characteristics[self.on_characteristic] = True
175 
176  if (
177  percentage is not None
178  and FanEntityFeature.SET_SPEED in self.supported_featuressupported_featuressupported_featuressupported_features
179  ):
180  characteristics[CharacteristicsTypes.ROTATION_SPEED] = round(
181  percentage_to_ranged_value(self._speed_range_speed_range, percentage)
182  )
183 
184  if characteristics:
185  await self.async_put_characteristicsasync_put_characteristics(characteristics)
186 
187  async def async_turn_off(self, **kwargs: Any) -> None:
188  """Turn the specified fan off."""
189  await self.async_put_characteristicsasync_put_characteristics({self.on_characteristic: False})
190 
191 
193  """Implement fan support for public.hap.service.fan."""
194 
195  on_characteristic = CharacteristicsTypes.ON
196 
197 
199  """Implement fan support for public.hap.service.fanv2."""
200 
201  on_characteristic = CharacteristicsTypes.ACTIVE
202 
203 
204 ENTITY_TYPES = {
205  ServicesTypes.FAN: HomeKitFanV1,
206  ServicesTypes.FAN_V2: HomeKitFanV2,
207  ServicesTypes.AIR_PURIFIER: HomeKitFanV2,
208 }
209 
210 
212  hass: HomeAssistant,
213  config_entry: ConfigEntry,
214  async_add_entities: AddEntitiesCallback,
215 ) -> None:
216  """Set up Homekit fans."""
217  hkid: str = config_entry.data["AccessoryPairingID"]
218  conn: HKDevice = hass.data[KNOWN_DEVICES][hkid]
219 
220  @callback
221  def async_add_service(service: Service) -> bool:
222  if not (entity_class := ENTITY_TYPES.get(service.type)):
223  return False
224  info = {"aid": service.accessory.aid, "iid": service.iid}
225  entity: HomeKitEntity = entity_class(conn, info)
226  conn.async_migrate_unique_id(
227  entity.old_unique_id, entity.unique_id, Platform.FAN
228  )
229  async_add_entities([entity])
230  return True
231 
232  conn.add_listener(async_add_service)
FanEntityFeature supported_features(self)
Definition: __init__.py:527
None async_put_characteristics(self, dict[str, Any] characteristics)
Definition: entity.py:125
None _async_clear_property_cache(self, tuple[str,...] properties)
Definition: entity.py:79
None async_turn_on(self, int|None percentage=None, str|None preset_mode=None, **Any kwargs)
Definition: fan.py:169
int|None supported_features(self)
Definition: entity.py:861
None async_turn_off(self, **Any kwargs)
Definition: entity.py:1709
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: fan.py:215
float percentage_to_ranged_value(tuple[float, float] low_high_range, float percentage)
Definition: percentage.py:81
int ranged_value_to_percentage(tuple[float, float] low_high_range, float value)
Definition: percentage.py:64