Home Assistant Unofficial Reference 2024.12.1
type_humidifiers.py
Go to the documentation of this file.
1 """Class to hold all thermostat accessories."""
2 
3 import logging
4 from typing import Any
5 
6 from pyhap.const import CATEGORY_HUMIDIFIER
7 from pyhap.util import callback as pyhap_callback
8 
10  ATTR_CURRENT_HUMIDITY,
11  ATTR_HUMIDITY,
12  ATTR_MAX_HUMIDITY,
13  ATTR_MIN_HUMIDITY,
14  DEFAULT_MAX_HUMIDITY,
15  DEFAULT_MIN_HUMIDITY,
16  DOMAIN as HUMIDIFIER_DOMAIN,
17  SERVICE_SET_HUMIDITY,
18  HumidifierDeviceClass,
19 )
20 from homeassistant.const import (
21  ATTR_DEVICE_CLASS,
22  ATTR_ENTITY_ID,
23  PERCENTAGE,
24  SERVICE_TURN_OFF,
25  SERVICE_TURN_ON,
26  STATE_ON,
27 )
28 from homeassistant.core import (
29  Event,
30  EventStateChangedData,
31  HassJobType,
32  State,
33  callback,
34 )
35 from homeassistant.helpers.event import async_track_state_change_event
36 
37 from .accessories import TYPES, HomeAccessory
38 from .const import (
39  CHAR_ACTIVE,
40  CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER,
41  CHAR_CURRENT_HUMIDITY,
42  CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY,
43  CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY,
44  CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER,
45  CONF_LINKED_HUMIDITY_SENSOR,
46  PROP_MAX_VALUE,
47  PROP_MIN_STEP,
48  PROP_MIN_VALUE,
49  SERV_HUMIDIFIER_DEHUMIDIFIER,
50 )
51 
52 _LOGGER = logging.getLogger(__name__)
53 
54 HC_HUMIDIFIER = 1
55 HC_DEHUMIDIFIER = 2
56 
57 HC_HASS_TO_HOMEKIT_DEVICE_CLASS = {
58  HumidifierDeviceClass.HUMIDIFIER: HC_HUMIDIFIER,
59  HumidifierDeviceClass.DEHUMIDIFIER: HC_DEHUMIDIFIER,
60 }
61 
62 HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME = {
63  HumidifierDeviceClass.HUMIDIFIER: "Humidifier",
64  HumidifierDeviceClass.DEHUMIDIFIER: "Dehumidifier",
65 }
66 
67 HC_DEVICE_CLASS_TO_TARGET_CHAR = {
68  HC_HUMIDIFIER: CHAR_HUMIDIFIER_THRESHOLD_HUMIDITY,
69  HC_DEHUMIDIFIER: CHAR_DEHUMIDIFIER_THRESHOLD_HUMIDITY,
70 }
71 
72 
73 HC_STATE_INACTIVE = 0
74 HC_STATE_IDLE = 1
75 HC_STATE_HUMIDIFYING = 2
76 HC_STATE_DEHUMIDIFYING = 3
77 
78 BASE_VALID_VALUES = {
79  "Inactive": HC_STATE_INACTIVE,
80  "Idle": HC_STATE_IDLE,
81 }
82 
83 VALID_VALUES_BY_DEVICE_CLASS = {
84  HumidifierDeviceClass.HUMIDIFIER: {
85  **BASE_VALID_VALUES,
86  "Humidifying": HC_STATE_HUMIDIFYING,
87  },
88  HumidifierDeviceClass.DEHUMIDIFIER: {
89  **BASE_VALID_VALUES,
90  "Dehumidifying": HC_STATE_DEHUMIDIFYING,
91  },
92 }
93 
94 
95 @TYPES.register("HumidifierDehumidifier")
97  """Generate a HumidifierDehumidifier accessory for a humidifier."""
98 
99  def __init__(self, *args: Any) -> None:
100  """Initialize a HumidifierDehumidifier accessory object."""
101  super().__init__(*args, category=CATEGORY_HUMIDIFIER)
102  self._reload_on_change_attrs_reload_on_change_attrs.extend(
103  (
104  ATTR_MAX_HUMIDITY,
105  ATTR_MIN_HUMIDITY,
106  )
107  )
108 
109  self.chars: list[str] = []
110  states = self.hasshass.states
111  state = states.get(self.entity_identity_id)
112  assert state
113  device_class = state.attributes.get(
114  ATTR_DEVICE_CLASS, HumidifierDeviceClass.HUMIDIFIER
115  )
116  self._hk_device_class_hk_device_class = HC_HASS_TO_HOMEKIT_DEVICE_CLASS[device_class]
117 
118  self._target_humidity_char_name_target_humidity_char_name = HC_DEVICE_CLASS_TO_TARGET_CHAR[
119  self._hk_device_class_hk_device_class
120  ]
121  self.chars.append(self._target_humidity_char_name_target_humidity_char_name)
122 
123  serv_humidifier_dehumidifier = self.add_preload_service(
124  SERV_HUMIDIFIER_DEHUMIDIFIER, self.chars
125  )
126 
127  # Current and target mode characteristics
128  self.char_current_humidifier_dehumidifierchar_current_humidifier_dehumidifier = (
129  serv_humidifier_dehumidifier.configure_char(
130  CHAR_CURRENT_HUMIDIFIER_DEHUMIDIFIER,
131  value=0,
132  valid_values=VALID_VALUES_BY_DEVICE_CLASS[device_class],
133  )
134  )
135  self.char_target_humidifier_dehumidifierchar_target_humidifier_dehumidifier = (
136  serv_humidifier_dehumidifier.configure_char(
137  CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER,
138  value=self._hk_device_class_hk_device_class,
139  properties={
140  PROP_MIN_VALUE: self._hk_device_class_hk_device_class,
141  PROP_MAX_VALUE: self._hk_device_class_hk_device_class,
142  },
143  valid_values={
144  HC_HASS_TO_HOMEKIT_DEVICE_CLASS_NAME[
145  device_class
146  ]: self._hk_device_class_hk_device_class
147  },
148  )
149  )
150 
151  # Current and target humidity characteristics
152  self.char_current_humiditychar_current_humidity = serv_humidifier_dehumidifier.configure_char(
153  CHAR_CURRENT_HUMIDITY, value=0
154  )
155 
156  self.char_target_humiditychar_target_humidity = serv_humidifier_dehumidifier.configure_char(
157  self._target_humidity_char_name_target_humidity_char_name,
158  value=45,
159  properties={
160  PROP_MIN_VALUE: DEFAULT_MIN_HUMIDITY,
161  PROP_MAX_VALUE: DEFAULT_MAX_HUMIDITY,
162  PROP_MIN_STEP: 1,
163  },
164  )
165 
166  # Active/inactive characteristics
167  self.char_activechar_active = serv_humidifier_dehumidifier.configure_char(
168  CHAR_ACTIVE, value=False
169  )
170 
171  self.async_update_stateasync_update_stateasync_update_state(state)
172 
173  serv_humidifier_dehumidifier.setter_callback = self._set_chars_set_chars
174 
175  self.linked_humidity_sensorlinked_humidity_sensor = self.configconfig.get(CONF_LINKED_HUMIDITY_SENSOR)
176  if self.linked_humidity_sensorlinked_humidity_sensor:
177  if humidity_state := states.get(self.linked_humidity_sensorlinked_humidity_sensor):
178  self._async_update_current_humidity_async_update_current_humidity(humidity_state)
179 
180  @callback
181  @pyhap_callback # type: ignore[misc]
182  def run(self) -> None:
183  """Handle accessory driver started event.
184 
185  Run inside the Home Assistant event loop.
186  """
187  if self.linked_humidity_sensorlinked_humidity_sensor:
188  self._subscriptions.append(
190  self.hasshass,
191  [self.linked_humidity_sensorlinked_humidity_sensor],
192  self.async_update_current_humidity_eventasync_update_current_humidity_event,
193  job_type=HassJobType.Callback,
194  )
195  )
196 
197  super().run()
198 
199  @callback
201  self, event: Event[EventStateChangedData]
202  ) -> None:
203  """Handle state change event listener callback."""
204  self._async_update_current_humidity_async_update_current_humidity(event.data["new_state"])
205 
206  @callback
207  def _async_update_current_humidity(self, new_state: State | None) -> None:
208  """Handle linked humidity sensor state change to update HomeKit value."""
209  if new_state is None:
210  _LOGGER.error(
211  (
212  "%s: Unable to update from linked humidity sensor %s: the entity"
213  " state is None"
214  ),
215  self.entity_identity_id,
216  self.linked_humidity_sensorlinked_humidity_sensor,
217  )
218  return
219  try:
220  current_humidity = float(new_state.state)
221  except ValueError as ex:
222  _LOGGER.debug(
223  "%s: Unable to update from linked humidity sensor %s: %s",
224  self.entity_identity_id,
225  self.linked_humidity_sensorlinked_humidity_sensor,
226  ex,
227  )
228  return
229  self._async_update_current_humidity_value_async_update_current_humidity_value(current_humidity)
230 
231  @callback
232  def _async_update_current_humidity_value(self, current_humidity: float) -> None:
233  """Handle linked humidity or built-in humidity."""
234  if self.char_current_humiditychar_current_humidity.value != current_humidity:
235  _LOGGER.debug(
236  "%s: Linked humidity sensor %s changed to %d",
237  self.entity_identity_id,
238  self.linked_humidity_sensorlinked_humidity_sensor,
239  current_humidity,
240  )
241  self.char_current_humiditychar_current_humidity.set_value(current_humidity)
242 
243  def _set_chars(self, char_values: dict[str, Any]) -> None:
244  """Set characteristics based on the data coming from HomeKit."""
245  _LOGGER.debug("HumidifierDehumidifier _set_chars: %s", char_values)
246 
247  if CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER in char_values:
248  hk_value = char_values[CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER]
249  if self._hk_device_class_hk_device_class != hk_value:
250  _LOGGER.error(
251  "%s is not supported", CHAR_TARGET_HUMIDIFIER_DEHUMIDIFIER
252  )
253 
254  if CHAR_ACTIVE in char_values:
255  self.async_call_serviceasync_call_service(
256  HUMIDIFIER_DOMAIN,
257  SERVICE_TURN_ON if char_values[CHAR_ACTIVE] else SERVICE_TURN_OFF,
258  {ATTR_ENTITY_ID: self.entity_identity_id},
259  f"{CHAR_ACTIVE} to {char_values[CHAR_ACTIVE]}",
260  )
261 
262  if self._target_humidity_char_name_target_humidity_char_name in char_values:
263  state = self.hasshass.states.get(self.entity_identity_id)
264  assert state
265  min_humidity, max_humidity = self.get_humidity_rangeget_humidity_range(state)
266  humidity = round(char_values[self._target_humidity_char_name_target_humidity_char_name])
267 
268  if (humidity < min_humidity) or (humidity > max_humidity):
269  humidity = min(max_humidity, max(min_humidity, humidity))
270  # Update the HomeKit value to the clamped humidity, so the user will get a visual feedback that they
271  # cannot not set to a value below/above the min/max.
272  self.char_target_humiditychar_target_humidity.set_value(humidity)
273 
274  self.async_call_serviceasync_call_service(
275  HUMIDIFIER_DOMAIN,
276  SERVICE_SET_HUMIDITY,
277  {ATTR_ENTITY_ID: self.entity_identity_id, ATTR_HUMIDITY: humidity},
278  (
279  f"{self._target_humidity_char_name} to "
280  f"{char_values[self._target_humidity_char_name]}{PERCENTAGE}"
281  ),
282  )
283 
284  def get_humidity_range(self, state: State) -> tuple[int, int]:
285  """Return min and max humidity range."""
286  attributes = state.attributes
287  min_humidity = max(
288  int(round(attributes.get(ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY))), 0
289  )
290  max_humidity = min(
291  int(round(attributes.get(ATTR_MAX_HUMIDITY, DEFAULT_MAX_HUMIDITY))), 100
292  )
293  return min_humidity, max_humidity
294 
295  @callback
296  def async_update_state(self, new_state: State) -> None:
297  """Update state without rechecking the device features."""
298  is_active = new_state.state == STATE_ON
299  attributes = new_state.attributes
300 
301  # Update active state
302  self.char_activechar_active.set_value(is_active)
303 
304  # Set current state
305  if is_active:
306  if self._hk_device_class_hk_device_class == HC_HUMIDIFIER:
307  current_state = HC_STATE_HUMIDIFYING
308  else:
309  current_state = HC_STATE_DEHUMIDIFYING
310  else:
311  current_state = HC_STATE_INACTIVE
312  self.char_current_humidifier_dehumidifierchar_current_humidifier_dehumidifier.set_value(current_state)
313 
314  # Update target humidity
315  target_humidity = attributes.get(ATTR_HUMIDITY)
316  if isinstance(target_humidity, (int, float)):
317  self.char_target_humiditychar_target_humidity.set_value(target_humidity)
318  current_humidity = attributes.get(ATTR_CURRENT_HUMIDITY)
319  if isinstance(current_humidity, (int, float)):
320  self.char_current_humiditychar_current_humidity.set_value(current_humidity)
None async_call_service(self, str domain, str service, dict[str, Any]|None service_data, Any|None value=None)
Definition: accessories.py:609
None async_update_current_humidity_event(self, Event[EventStateChangedData] event)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
CALLBACK_TYPE async_track_state_change_event(HomeAssistant hass, str|Iterable[str] entity_ids, Callable[[Event[EventStateChangedData]], Any] action, HassJobType|None job_type=None)
Definition: event.py:314