Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Xiaomi Bluetooth integration."""
2 
3 from __future__ import annotations
4 
5 from functools import partial
6 import logging
7 from typing import cast
8 
9 from xiaomi_ble import EncryptionScheme, SensorUpdate, XiaomiBluetoothDeviceData
10 
12  DOMAIN as BLUETOOTH_DOMAIN,
13  BluetoothScanningMode,
14  BluetoothServiceInfoBleak,
15  async_ble_device_from_address,
16 )
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import Platform
19 from homeassistant.core import CoreState, HomeAssistant
20 from homeassistant.helpers import device_registry as dr
21 from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceRegistry
22 from homeassistant.helpers.dispatcher import async_dispatcher_send
23 
24 from .const import (
25  CONF_DISCOVERED_EVENT_CLASSES,
26  CONF_SLEEPY_DEVICE,
27  DOMAIN,
28  XIAOMI_BLE_EVENT,
29  XiaomiBleEvent,
30 )
31 from .coordinator import XiaomiActiveBluetoothProcessorCoordinator
32 from .types import XiaomiBLEConfigEntry
33 
34 PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.EVENT, Platform.SENSOR]
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 
40  hass: HomeAssistant,
41  entry: XiaomiBLEConfigEntry,
42  device_registry: DeviceRegistry,
43  service_info: BluetoothServiceInfoBleak,
44 ) -> SensorUpdate:
45  """Process a BluetoothServiceInfoBleak, running side effects and returning sensor data."""
46  coordinator = entry.runtime_data
47  data = coordinator.device_data
48  update = data.update(service_info)
49  discovered_event_classes = coordinator.discovered_event_classes
50  if entry.data.get(CONF_SLEEPY_DEVICE, False) != data.sleepy_device:
51  hass.config_entries.async_update_entry(
52  entry,
53  data=entry.data | {CONF_SLEEPY_DEVICE: data.sleepy_device},
54  )
55  if update.events:
56  address = service_info.device.address
57  for device_key, event in update.events.items():
58  sensor_device_info = update.devices[device_key.device_id]
59  device = device_registry.async_get_or_create(
60  config_entry_id=entry.entry_id,
61  connections={(CONNECTION_BLUETOOTH, address)},
62  identifiers={(BLUETOOTH_DOMAIN, address)},
63  manufacturer=sensor_device_info.manufacturer,
64  model=sensor_device_info.model,
65  name=sensor_device_info.name,
66  sw_version=sensor_device_info.sw_version,
67  hw_version=sensor_device_info.hw_version,
68  )
69  # event_class may be postfixed with a number, ie 'button_2'
70  # but if there is only one button then it will be 'button'
71  event_class = event.device_key.key
72  event_type = event.event_type
73 
74  ble_event = XiaomiBleEvent(
75  device_id=device.id,
76  address=address,
77  event_class=event_class, # ie 'button'
78  event_type=event_type, # ie 'press'
79  event_properties=event.event_properties,
80  )
81 
82  if event_class not in discovered_event_classes:
83  discovered_event_classes.add(event_class)
84  hass.config_entries.async_update_entry(
85  entry,
86  data=entry.data
87  | {CONF_DISCOVERED_EVENT_CLASSES: list(discovered_event_classes)},
88  )
90  hass, format_discovered_event_class(address), event_class, ble_event
91  )
92 
93  hass.bus.async_fire(XIAOMI_BLE_EVENT, cast(dict, ble_event))
95  hass,
96  format_event_dispatcher_name(address, event_class),
97  ble_event,
98  )
99 
100  # If device isn't pending we know it has seen at least one broadcast with a payload
101  # If that payload was encrypted and the bindkey was not verified then we need to reauth
102  if (
103  not data.pending
104  and data.encryption_scheme != EncryptionScheme.NONE
105  and not data.bindkey_verified
106  ):
107  entry.async_start_reauth(hass, data={"device": data})
108 
109  return update
110 
111 
112 def format_event_dispatcher_name(address: str, event_class: str) -> str:
113  """Format an event dispatcher name."""
114  return f"{DOMAIN}_event_{address}_{event_class}"
115 
116 
117 def format_discovered_event_class(address: str) -> str:
118  """Format a discovered event class."""
119  return f"{DOMAIN}_discovered_event_class_{address}"
120 
121 
122 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
123  """Set up Xiaomi BLE device from a config entry."""
124  address = entry.unique_id
125  assert address is not None
126 
127  kwargs = {}
128  if bindkey := entry.data.get("bindkey"):
129  kwargs["bindkey"] = bytes.fromhex(bindkey)
130  data = XiaomiBluetoothDeviceData(**kwargs)
131 
132  def _needs_poll(
133  service_info: BluetoothServiceInfoBleak, last_poll: float | None
134  ) -> bool:
135  # Only poll if hass is running, we need to poll,
136  # and we actually have a way to connect to the device
137  return (
138  hass.state is CoreState.running
139  and data.poll_needed(service_info, last_poll)
140  and bool(
142  hass, service_info.device.address, connectable=True
143  )
144  )
145  )
146 
147  async def _async_poll(service_info: BluetoothServiceInfoBleak) -> SensorUpdate:
148  # BluetoothServiceInfoBleak is defined in HA, otherwise would just pass it
149  # directly to the Xiaomi code
150  # Make sure the device we have is one that we can connect with
151  # in case its coming from a passive scanner
152  if service_info.connectable:
153  connectable_device = service_info.device
154  elif device := async_ble_device_from_address(
155  hass, service_info.device.address, True
156  ):
157  connectable_device = device
158  else:
159  # We have no bluetooth controller that is in range of
160  # the device to poll it
161  raise RuntimeError(
162  f"No connectable device found for {service_info.device.address}"
163  )
164  return await data.async_poll(connectable_device)
165 
166  device_registry = dr.async_get(hass)
168  hass,
169  _LOGGER,
170  address=address,
171  mode=BluetoothScanningMode.PASSIVE,
172  update_method=partial(process_service_info, hass, entry, device_registry),
173  needs_poll_method=_needs_poll,
174  device_data=data,
175  discovered_event_classes=set(entry.data.get(CONF_DISCOVERED_EVENT_CLASSES, [])),
176  poll_method=_async_poll,
177  # We will take advertisements from non-connectable devices
178  # since we will trade the BLEDevice for a connectable one
179  # if we need to poll it
180  connectable=False,
181  entry=entry,
182  )
183  entry.runtime_data = coordinator
184  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
185  # only start after all platforms have had a chance to subscribe
186  entry.async_on_unload(coordinator.async_start())
187  return True
188 
189 
190 async def async_unload_entry(hass: HomeAssistant, entry: XiaomiBLEConfigEntry) -> bool:
191  """Unload a config entry."""
192  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
BLEDevice|None async_ble_device_from_address(HomeAssistant hass, str address, bool connectable=True)
Definition: api.py:88
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:122
str format_discovered_event_class(str address)
Definition: __init__.py:117
SensorUpdate process_service_info(HomeAssistant hass, XiaomiBLEConfigEntry entry, DeviceRegistry device_registry, BluetoothServiceInfoBleak service_info)
Definition: __init__.py:44
bool async_unload_entry(HomeAssistant hass, XiaomiBLEConfigEntry entry)
Definition: __init__.py:190
str format_event_dispatcher_name(str address, str event_class)
Definition: __init__.py:112
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193