Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """The Yale Access Bluetooth integration."""
2 
3 from __future__ import annotations
4 
5 from yalexs_ble import (
6  AuthError,
7  ConnectionInfo,
8  LockInfo,
9  LockState,
10  PushLock,
11  YaleXSBLEError,
12  close_stale_connections_by_address,
13  local_name_is_unique,
14 )
15 
16 from homeassistant.components import bluetooth
17 from homeassistant.config_entries import ConfigEntry
18 from homeassistant.const import CONF_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform
19 from homeassistant.core import CALLBACK_TYPE, CoreState, Event, HomeAssistant, callback
20 from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
21 
22 from .const import (
23  CONF_ALWAYS_CONNECTED,
24  CONF_KEY,
25  CONF_LOCAL_NAME,
26  CONF_SLOT,
27  DEVICE_TIMEOUT,
28 )
29 from .models import YaleXSBLEData
30 from .util import async_find_existing_service_info, bluetooth_callback_matcher
31 
32 type YALEXSBLEConfigEntry = ConfigEntry[YaleXSBLEData]
33 
34 
35 PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.LOCK, Platform.SENSOR]
36 
37 
38 async def async_setup_entry(hass: HomeAssistant, entry: YALEXSBLEConfigEntry) -> bool:
39  """Set up Yale Access Bluetooth from a config entry."""
40  local_name = entry.data[CONF_LOCAL_NAME]
41  address = entry.data[CONF_ADDRESS]
42  key = entry.data[CONF_KEY]
43  slot = entry.data[CONF_SLOT]
44  has_unique_local_name = local_name_is_unique(local_name)
45  always_connected = entry.options.get(CONF_ALWAYS_CONNECTED, False)
46  push_lock = PushLock(
47  local_name, address, None, key, slot, always_connected=always_connected
48  )
49  id_ = local_name if has_unique_local_name else address
50  push_lock.set_name(f"{entry.title} ({id_})")
51 
52  # Ensure any lingering connections are closed since the device may not be
53  # advertising when its connected to another client which will prevent us
54  # from setting the device and setup will fail.
55  await close_stale_connections_by_address(address)
56 
57  @callback
58  def _async_update_ble(
59  service_info: bluetooth.BluetoothServiceInfoBleak,
60  change: bluetooth.BluetoothChange,
61  ) -> None:
62  """Update from a ble callback."""
63  push_lock.update_advertisement(service_info.device, service_info.advertisement)
64 
65  shutdown_callback: CALLBACK_TYPE | None = await push_lock.start()
66 
67  @callback
68  def _async_shutdown(event: Event | None = None) -> None:
69  nonlocal shutdown_callback
70  if shutdown_callback:
71  shutdown_callback()
72  shutdown_callback = None
73 
74  entry.async_on_unload(_async_shutdown)
75 
76  # We may already have the advertisement, so check for it.
77  if service_info := async_find_existing_service_info(hass, local_name, address):
78  push_lock.update_advertisement(service_info.device, service_info.advertisement)
79  elif hass.state is CoreState.starting:
80  # If we are starting and the advertisement is not found, do not delay
81  # the setup. We will wait for the advertisement to be found and then
82  # discovery will trigger setup retry.
83  raise ConfigEntryNotReady("{local_name} ({address}) not advertising yet")
84 
85  entry.async_on_unload(
86  bluetooth.async_register_callback(
87  hass,
88  _async_update_ble,
89  bluetooth_callback_matcher(local_name, push_lock.address),
90  bluetooth.BluetoothScanningMode.PASSIVE,
91  )
92  )
93 
94  try:
95  await push_lock.wait_for_first_update(DEVICE_TIMEOUT)
96  except AuthError as ex:
97  raise ConfigEntryAuthFailed(str(ex)) from ex
98  except (YaleXSBLEError, TimeoutError) as ex:
99  raise ConfigEntryNotReady(
100  f"{ex}; Try moving the Bluetooth adapter closer to {local_name}"
101  ) from ex
102 
103  entry.runtime_data = YaleXSBLEData(entry.title, push_lock, always_connected)
104 
105  @callback
106  def _async_device_unavailable(
107  _service_info: bluetooth.BluetoothServiceInfoBleak,
108  ) -> None:
109  """Handle device not longer being seen by the bluetooth stack."""
110  push_lock.reset_advertisement_state()
111 
112  entry.async_on_unload(
113  bluetooth.async_track_unavailable(
114  hass, _async_device_unavailable, push_lock.address
115  )
116  )
117 
118  @callback
119  def _async_state_changed(
120  new_state: LockState, lock_info: LockInfo, connection_info: ConnectionInfo
121  ) -> None:
122  """Handle state changed."""
123  if new_state.auth and not new_state.auth.successful:
124  entry.async_start_reauth(hass)
125 
126  entry.async_on_unload(push_lock.register_callback(_async_state_changed))
127  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
128  entry.async_on_unload(entry.add_update_listener(_async_update_listener))
129  entry.async_on_unload(
130  hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_shutdown)
131  )
132  return True
133 
134 
136  hass: HomeAssistant, entry: YALEXSBLEConfigEntry
137 ) -> None:
138  """Handle options update."""
139  data = entry.runtime_data
140  if entry.title != data.title or data.always_connected != entry.options.get(
141  CONF_ALWAYS_CONNECTED
142  ):
143  await hass.config_entries.async_reload(entry.entry_id)
144 
145 
146 async def async_unload_entry(hass: HomeAssistant, entry: YALEXSBLEConfigEntry) -> bool:
147  """Unload a config entry."""
148  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
BluetoothServiceInfoBleak|None async_find_existing_service_info(HomeAssistant hass, str local_name, str address)
Definition: util.py:39
BluetoothCallbackMatcher bluetooth_callback_matcher(str local_name, str address)
Definition: util.py:23
bool async_unload_entry(HomeAssistant hass, YALEXSBLEConfigEntry entry)
Definition: __init__.py:146
None _async_update_listener(HomeAssistant hass, YALEXSBLEConfigEntry entry)
Definition: __init__.py:137
bool async_setup_entry(HomeAssistant hass, YALEXSBLEConfigEntry entry)
Definition: __init__.py:38