Home Assistant Unofficial Reference 2024.12.1
binary_sensor.py
Go to the documentation of this file.
1 """Support for Xiaomi aqara binary sensors."""
2 
3 import logging
4 
6  BinarySensorDeviceClass,
7  BinarySensorEntity,
8 )
9 from homeassistant.config_entries import ConfigEntry
10 from homeassistant.core import HomeAssistant, callback
11 from homeassistant.helpers.entity_platform import AddEntitiesCallback
12 from homeassistant.helpers.event import async_call_later
13 from homeassistant.helpers.restore_state import RestoreEntity
14 
15 from .const import DOMAIN, GATEWAYS_KEY
16 from .entity import XiaomiDevice
17 
18 _LOGGER = logging.getLogger(__name__)
19 
20 NO_CLOSE = "no_close"
21 ATTR_OPEN_SINCE = "Open since"
22 
23 MOTION = "motion"
24 NO_MOTION = "no_motion"
25 ATTR_LAST_ACTION = "last_action"
26 ATTR_NO_MOTION_SINCE = "No motion since"
27 
28 DENSITY = "density"
29 ATTR_DENSITY = "Density"
30 
31 
33  hass: HomeAssistant,
34  config_entry: ConfigEntry,
35  async_add_entities: AddEntitiesCallback,
36 ) -> None:
37  """Perform the setup for Xiaomi devices."""
38  entities: list[XiaomiBinarySensor] = []
39  gateway = hass.data[DOMAIN][GATEWAYS_KEY][config_entry.entry_id]
40  for entity in gateway.devices["binary_sensor"]:
41  model = entity["model"]
42  if model in ("motion", "sensor_motion", "sensor_motion.aq2"):
43  entities.append(XiaomiMotionSensor(entity, hass, gateway, config_entry))
44  elif model in ("magnet", "sensor_magnet", "sensor_magnet.aq2"):
45  entities.append(XiaomiDoorSensor(entity, gateway, config_entry))
46  elif model == "sensor_wleak.aq1":
47  entities.append(XiaomiWaterLeakSensor(entity, gateway, config_entry))
48  elif model in ("smoke", "sensor_smoke"):
49  entities.append(XiaomiSmokeSensor(entity, gateway, config_entry))
50  elif model in ("natgas", "sensor_natgas"):
51  entities.append(XiaomiNatgasSensor(entity, gateway, config_entry))
52  elif model in (
53  "switch",
54  "sensor_switch",
55  "sensor_switch.aq2",
56  "sensor_switch.aq3",
57  "remote.b1acn01",
58  ):
59  if "proto" not in entity or int(entity["proto"][0:1]) == 1:
60  data_key = "status"
61  else:
62  data_key = "button_0"
63  entities.append(
64  XiaomiButton(entity, "Switch", data_key, hass, gateway, config_entry)
65  )
66  elif model in (
67  "86sw1",
68  "sensor_86sw1",
69  "sensor_86sw1.aq1",
70  "remote.b186acn01",
71  "remote.b186acn02",
72  ):
73  if "proto" not in entity or int(entity["proto"][0:1]) == 1:
74  data_key = "channel_0"
75  else:
76  data_key = "button_0"
77  entities.append(
79  entity, "Wall Switch", data_key, hass, gateway, config_entry
80  )
81  )
82  elif model in (
83  "86sw2",
84  "sensor_86sw2",
85  "sensor_86sw2.aq1",
86  "remote.b286acn01",
87  "remote.b286acn02",
88  ):
89  if "proto" not in entity or int(entity["proto"][0:1]) == 1:
90  data_key_left = "channel_0"
91  data_key_right = "channel_1"
92  else:
93  data_key_left = "button_0"
94  data_key_right = "button_1"
95  entities.append(
97  entity,
98  "Wall Switch (Left)",
99  data_key_left,
100  hass,
101  gateway,
102  config_entry,
103  )
104  )
105  entities.append(
106  XiaomiButton(
107  entity,
108  "Wall Switch (Right)",
109  data_key_right,
110  hass,
111  gateway,
112  config_entry,
113  )
114  )
115  entities.append(
116  XiaomiButton(
117  entity,
118  "Wall Switch (Both)",
119  "dual_channel",
120  hass,
121  gateway,
122  config_entry,
123  )
124  )
125  elif model in ("cube", "sensor_cube", "sensor_cube.aqgl01"):
126  entities.append(XiaomiCube(entity, hass, gateway, config_entry))
127  elif model in ("vibration", "vibration.aq1"):
128  entities.append(
129  XiaomiVibration(entity, "Vibration", "status", gateway, config_entry)
130  )
131  else:
132  _LOGGER.warning("Unmapped Device Model %s", model)
133 
134  async_add_entities(entities)
135 
136 
138  """Representation of a base XiaomiBinarySensor."""
139 
140  def __init__(self, device, name, xiaomi_hub, data_key, device_class, config_entry):
141  """Initialize the XiaomiSmokeSensor."""
142  self._data_key_data_key = data_key
143  self._device_class_device_class = device_class
144  self._density_density = 0
145  super().__init__(device, name, xiaomi_hub, config_entry)
146 
147  @property
148  def is_on(self):
149  """Return true if sensor is on."""
150  return self._state_state
151 
152  @property
153  def device_class(self):
154  """Return the class of binary sensor."""
155  return self._device_class_device_class
156 
157  def update(self) -> None:
158  """Update the sensor state."""
159  _LOGGER.debug("Updating xiaomi sensor (%s) by polling", self._sid_sid)
160  self._get_from_hub_get_from_hub(self._sid_sid)
161 
162 
164  """Representation of a XiaomiNatgasSensor."""
165 
166  def __init__(self, device, xiaomi_hub, config_entry):
167  """Initialize the XiaomiSmokeSensor."""
168  self._density_density_density = None
169  super().__init__(
170  device, "Natgas Sensor", xiaomi_hub, "alarm", "gas", config_entry
171  )
172 
173  @property
175  """Return the state attributes."""
176  attrs = {ATTR_DENSITY: self._density_density_density}
177  attrs.update(super().extra_state_attributes)
178  return attrs
179 
180  async def async_added_to_hass(self) -> None:
181  """Handle entity which will be added."""
182  await super().async_added_to_hass()
183  self._state_state_state = False
184 
185  def parse_data(self, data, raw_data):
186  """Parse data sent by gateway."""
187  if DENSITY in data:
188  self._density_density_density = int(data.get(DENSITY))
189 
190  value = data.get(self._data_key_data_key)
191  if value is None:
192  return False
193 
194  if value in ("1", "2"):
195  if self._state_state_state:
196  return False
197  self._state_state_state = True
198  return True
199  if value == "0":
200  if self._state_state_state:
201  self._state_state_state = False
202  return True
203  return False
204 
205  return False
206 
207 
209  """Representation of a XiaomiMotionSensor."""
210 
211  def __init__(self, device, hass, xiaomi_hub, config_entry):
212  """Initialize the XiaomiMotionSensor."""
213  self._hass_hass = hass
214  self._no_motion_since_no_motion_since = 0
215  self._unsub_set_no_motion_unsub_set_no_motion = None
216  if "proto" not in device or int(device["proto"][0:1]) == 1:
217  data_key = "status"
218  else:
219  data_key = "motion_status"
220  super().__init__(
221  device, "Motion Sensor", xiaomi_hub, data_key, "motion", config_entry
222  )
223 
224  @property
226  """Return the state attributes."""
227  attrs = {ATTR_NO_MOTION_SINCE: self._no_motion_since_no_motion_since}
228  attrs.update(super().extra_state_attributes)
229  return attrs
230 
231  @callback
232  def _async_set_no_motion(self, now):
233  """Set state to False."""
234  self._unsub_set_no_motion_unsub_set_no_motion = None
235  self._state_state_state = False
236  self.async_write_ha_stateasync_write_ha_state()
237 
238  async def async_added_to_hass(self) -> None:
239  """Handle entity which will be added."""
240  await super().async_added_to_hass()
241  self._state_state_state = False
242 
243  def parse_data(self, data, raw_data):
244  """Parse data sent by gateway.
245 
246  Polling (proto v1, firmware version 1.4.1_159.0143)
247 
248  >> { "cmd":"read","sid":"158..."}
249  << {'model': 'motion', 'sid': '158...', 'short_id': 26331,
250  'cmd': 'read_ack', 'data': '{"voltage":3005}'}
251 
252  Multicast messages (proto v1, firmware version 1.4.1_159.0143)
253 
254  << {'model': 'motion', 'sid': '158...', 'short_id': 26331,
255  'cmd': 'report', 'data': '{"status":"motion"}'}
256  << {'model': 'motion', 'sid': '158...', 'short_id': 26331,
257  'cmd': 'report', 'data': '{"no_motion":"120"}'}
258  << {'model': 'motion', 'sid': '158...', 'short_id': 26331,
259  'cmd': 'report', 'data': '{"no_motion":"180"}'}
260  << {'model': 'motion', 'sid': '158...', 'short_id': 26331,
261  'cmd': 'report', 'data': '{"no_motion":"300"}'}
262  << {'model': 'motion', 'sid': '158...', 'short_id': 26331,
263  'cmd': 'heartbeat', 'data': '{"voltage":3005}'}
264 
265  """
266  if raw_data["cmd"] == "heartbeat":
267  _LOGGER.debug(
268  "Skipping heartbeat of the motion sensor. "
269  "It can introduce an incorrect state because of a firmware "
270  "bug (https://github.com/home-assistant/core/pull/"
271  "11631#issuecomment-357507744)"
272  )
273  return None
274 
275  if NO_MOTION in data:
276  self._no_motion_since_no_motion_since = data[NO_MOTION]
277  self._state_state_state = False
278  return True
279 
280  value = data.get(self._data_key_data_key_data_key)
281  if value is None:
282  return False
283 
284  if value == MOTION:
285  if self._data_key_data_key_data_key == "motion_status":
286  if self._unsub_set_no_motion_unsub_set_no_motion:
287  self._unsub_set_no_motion_unsub_set_no_motion()
288  self._unsub_set_no_motion_unsub_set_no_motion = async_call_later(
289  self._hass_hass, 120, self._async_set_no_motion_async_set_no_motion
290  )
291 
292  if self.entity_identity_id is not None:
293  self._hass_hass.bus.async_fire(
294  "xiaomi_aqara.motion", {"entity_id": self.entity_identity_id}
295  )
296 
297  self._no_motion_since_no_motion_since = 0
298  if self._state_state_state:
299  return False
300  self._state_state_state = True
301  return True
302 
303  return False
304 
305 
307  """Representation of a XiaomiDoorSensor."""
308 
309  def __init__(self, device, xiaomi_hub, config_entry):
310  """Initialize the XiaomiDoorSensor."""
311  self._open_since_open_since = 0
312  if "proto" not in device or int(device["proto"][0:1]) == 1:
313  data_key = "status"
314  else:
315  data_key = "window_status"
316  super().__init__(
317  device,
318  "Door Window Sensor",
319  xiaomi_hub,
320  data_key,
321  BinarySensorDeviceClass.OPENING,
322  config_entry,
323  )
324 
325  @property
327  """Return the state attributes."""
328  attrs = {ATTR_OPEN_SINCE: self._open_since_open_since}
329  attrs.update(super().extra_state_attributes)
330  return attrs
331 
332  async def async_added_to_hass(self) -> None:
333  """Handle entity which will be added."""
334  await super().async_added_to_hass()
335  if (state := await self.async_get_last_stateasync_get_last_state()) is None:
336  return
337 
338  self._state_state_state = state.state == "on"
339 
340  def parse_data(self, data, raw_data):
341  """Parse data sent by gateway."""
342  self._attr_should_poll_attr_should_poll_attr_should_poll = False
343  if NO_CLOSE in data: # handle push from the hub
344  self._open_since_open_since = data[NO_CLOSE]
345  return True
346 
347  value = data.get(self._data_key_data_key)
348  if value is None:
349  return False
350 
351  if value == "open":
352  self._attr_should_poll_attr_should_poll_attr_should_poll = True
353  if self._state_state_state:
354  return False
355  self._state_state_state = True
356  return True
357  if value == "close":
358  self._open_since_open_since = 0
359  if self._state_state_state:
360  self._state_state_state = False
361  return True
362  return False
363 
364  return False
365 
366 
368  """Representation of a XiaomiWaterLeakSensor."""
369 
370  def __init__(self, device, xiaomi_hub, config_entry):
371  """Initialize the XiaomiWaterLeakSensor."""
372  if "proto" not in device or int(device["proto"][0:1]) == 1:
373  data_key = "status"
374  else:
375  data_key = "wleak_status"
376  super().__init__(
377  device,
378  "Water Leak Sensor",
379  xiaomi_hub,
380  data_key,
381  BinarySensorDeviceClass.MOISTURE,
382  config_entry,
383  )
384 
385  async def async_added_to_hass(self) -> None:
386  """Handle entity which will be added."""
387  await super().async_added_to_hass()
388  self._state_state_state = False
389 
390  def parse_data(self, data, raw_data):
391  """Parse data sent by gateway."""
392  self._attr_should_poll_attr_should_poll_attr_should_poll = False
393 
394  value = data.get(self._data_key_data_key)
395  if value is None:
396  return False
397 
398  if value == "leak":
399  self._attr_should_poll_attr_should_poll_attr_should_poll = True
400  if self._state_state_state:
401  return False
402  self._state_state_state = True
403  return True
404  if value == "no_leak":
405  if self._state_state_state:
406  self._state_state_state = False
407  return True
408  return False
409 
410  return False
411 
412 
414  """Representation of a XiaomiSmokeSensor."""
415 
416  def __init__(self, device, xiaomi_hub, config_entry):
417  """Initialize the XiaomiSmokeSensor."""
418  self._density_density_density = 0
419  super().__init__(
420  device, "Smoke Sensor", xiaomi_hub, "alarm", "smoke", config_entry
421  )
422 
423  @property
425  """Return the state attributes."""
426  attrs = {ATTR_DENSITY: self._density_density_density}
427  attrs.update(super().extra_state_attributes)
428  return attrs
429 
430  async def async_added_to_hass(self) -> None:
431  """Handle entity which will be added."""
432  await super().async_added_to_hass()
433  self._state_state_state = False
434 
435  def parse_data(self, data, raw_data):
436  """Parse data sent by gateway."""
437  if DENSITY in data:
438  self._density_density_density = int(data.get(DENSITY))
439  value = data.get(self._data_key_data_key)
440  if value is None:
441  return False
442 
443  if value in ("1", "2"):
444  if self._state_state_state:
445  return False
446  self._state_state_state = True
447  return True
448  if value == "0":
449  if self._state_state_state:
450  self._state_state_state = False
451  return True
452  return False
453 
454  return False
455 
456 
458  """Representation of a Xiaomi Vibration Sensor."""
459 
460  def __init__(self, device, name, data_key, xiaomi_hub, config_entry):
461  """Initialize the XiaomiVibration."""
462  self._last_action_last_action = None
463  super().__init__(device, name, xiaomi_hub, data_key, None, config_entry)
464 
465  @property
467  """Return the state attributes."""
468  attrs = {ATTR_LAST_ACTION: self._last_action_last_action}
469  attrs.update(super().extra_state_attributes)
470  return attrs
471 
472  async def async_added_to_hass(self) -> None:
473  """Handle entity which will be added."""
474  await super().async_added_to_hass()
475  self._state_state_state = False
476 
477  def parse_data(self, data, raw_data):
478  """Parse data sent by gateway."""
479  value = data.get(self._data_key_data_key)
480  if value is None:
481  return False
482 
483  if value not in ("vibrate", "tilt", "free_fall", "actively"):
484  _LOGGER.warning("Unsupported movement_type detected: %s", value)
485  return False
486 
487  self.hasshass.bus.async_fire(
488  "xiaomi_aqara.movement",
489  {"entity_id": self.entity_identity_id, "movement_type": value},
490  )
491  self._last_action_last_action = value
492 
493  return True
494 
495 
497  """Representation of a Xiaomi Button."""
498 
499  def __init__(self, device, name, data_key, hass, xiaomi_hub, config_entry):
500  """Initialize the XiaomiButton."""
501  self._hass_hass = hass
502  self._last_action_last_action = None
503  super().__init__(device, name, xiaomi_hub, data_key, None, config_entry)
504 
505  @property
507  """Return the state attributes."""
508  attrs = {ATTR_LAST_ACTION: self._last_action_last_action}
509  attrs.update(super().extra_state_attributes)
510  return attrs
511 
512  async def async_added_to_hass(self) -> None:
513  """Handle entity which will be added."""
514  await super().async_added_to_hass()
515  self._state_state_state = False
516 
517  def parse_data(self, data, raw_data):
518  """Parse data sent by gateway."""
519  value = data.get(self._data_key_data_key)
520  if value is None:
521  return False
522 
523  if value == "long_click_press":
524  self._state_state_state = True
525  click_type = "long_click_press"
526  elif value == "long_click_release":
527  self._state_state_state = False
528  click_type = "hold"
529  elif value == "click":
530  click_type = "single"
531  elif value == "double_click":
532  click_type = "double"
533  elif value == "both_click":
534  click_type = "both"
535  elif value == "double_both_click":
536  click_type = "double_both"
537  elif value == "shake":
538  click_type = "shake"
539  elif value == "long_click":
540  click_type = "long"
541  elif value == "long_both_click":
542  click_type = "long_both"
543  else:
544  _LOGGER.warning("Unsupported click_type detected: %s", value)
545  return False
546 
547  self._hass_hass.bus.async_fire(
548  "xiaomi_aqara.click",
549  {"entity_id": self.entity_identity_id, "click_type": click_type},
550  )
551  self._last_action_last_action = click_type
552 
553  return True
554 
555 
557  """Representation of a Xiaomi Cube."""
558 
559  def __init__(self, device, hass, xiaomi_hub, config_entry):
560  """Initialize the Xiaomi Cube."""
561  self._hass_hass = hass
562  self._last_action_last_action = None
563  if "proto" not in device or int(device["proto"][0:1]) == 1:
564  data_key = "status"
565  else:
566  data_key = "cube_status"
567  super().__init__(device, "Cube", xiaomi_hub, data_key, None, config_entry)
568 
569  @property
571  """Return the state attributes."""
572  attrs = {ATTR_LAST_ACTION: self._last_action_last_action}
573  attrs.update(super().extra_state_attributes)
574  return attrs
575 
576  async def async_added_to_hass(self) -> None:
577  """Handle entity which will be added."""
578  await super().async_added_to_hass()
579  self._state_state_state = False
580 
581  def parse_data(self, data, raw_data):
582  """Parse data sent by gateway."""
583  if self._data_key_data_key in data:
584  self._hass_hass.bus.async_fire(
585  "xiaomi_aqara.cube_action",
586  {"entity_id": self.entity_identity_id, "action_type": data[self._data_key_data_key]},
587  )
588  self._last_action_last_action = data[self._data_key_data_key]
589 
590  if "rotate" in data:
591  action_value = float(
592  data["rotate"]
593  if isinstance(data["rotate"], int)
594  else data["rotate"].replace(",", ".")
595  )
596  self._hass_hass.bus.async_fire(
597  "xiaomi_aqara.cube_action",
598  {
599  "entity_id": self.entity_identity_id,
600  "action_type": "rotate",
601  "action_value": action_value,
602  },
603  )
604  self._last_action_last_action = "rotate"
605 
606  if "rotate_degree" in data:
607  action_value = float(
608  data["rotate_degree"]
609  if isinstance(data["rotate_degree"], int)
610  else data["rotate_degree"].replace(",", ".")
611  )
612  self._hass_hass.bus.async_fire(
613  "xiaomi_aqara.cube_action",
614  {
615  "entity_id": self.entity_identity_id,
616  "action_type": "rotate",
617  "action_value": action_value,
618  },
619  )
620  self._last_action_last_action = "rotate"
621 
622  return True
def __init__(self, device, name, xiaomi_hub, data_key, device_class, config_entry)
def __init__(self, device, name, data_key, hass, xiaomi_hub, config_entry)
def __init__(self, device, hass, xiaomi_hub, config_entry)
def __init__(self, device, hass, xiaomi_hub, config_entry)
def __init__(self, device, name, data_key, xiaomi_hub, config_entry)
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597