Home Assistant Unofficial Reference 2024.12.1
entry_manager.py
Go to the documentation of this file.
1 """Manager to set up IO with Crownstone devices for a config entry."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from crownstone_cloud import CrownstoneCloud
9 from crownstone_cloud.exceptions import (
10  CrownstoneAuthenticationError,
11  CrownstoneUnknownError,
12 )
13 from crownstone_sse import CrownstoneSSEAsync
14 from crownstone_uart import CrownstoneUart, UartEventBus
15 from crownstone_uart.Exceptions import UartException
16 
17 from homeassistant.components import persistent_notification
18 from homeassistant.config_entries import ConfigEntry
19 from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
20 from homeassistant.core import Event, HomeAssistant, callback
21 from homeassistant.exceptions import ConfigEntryNotReady
22 from homeassistant.helpers import aiohttp_client
23 from homeassistant.helpers.dispatcher import async_dispatcher_send
24 
25 from .const import (
26  CONF_USB_PATH,
27  CONF_USB_SPHERE,
28  DOMAIN,
29  PLATFORMS,
30  PROJECT_NAME,
31  SSE_LISTENERS,
32  UART_LISTENERS,
33 )
34 from .helpers import get_port
35 from .listeners import setup_sse_listeners, setup_uart_listeners
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 
41  """Manage a Crownstone config entry."""
42 
43  uart: CrownstoneUart | None = None
44  cloud: CrownstoneCloud
45  sse: CrownstoneSSEAsync
46 
47  def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
48  """Initialize the hub."""
49  self.hasshass = hass
50  self.config_entryconfig_entry = config_entry
51  self.listeners: dict[str, Any] = {}
52  self.usb_sphere_idusb_sphere_id: str | None = None
53 
54  async def async_setup(self) -> bool:
55  """Set up a Crownstone config entry.
56 
57  Returns True if the setup was successful.
58  """
59  email = self.config_entryconfig_entry.data[CONF_EMAIL]
60  password = self.config_entryconfig_entry.data[CONF_PASSWORD]
61 
62  self.cloudcloud = CrownstoneCloud(
63  email=email,
64  password=password,
65  clientsession=aiohttp_client.async_get_clientsession(self.hasshass),
66  )
67  # Login & sync all user data
68  try:
69  await self.cloudcloud.async_initialize()
70  except CrownstoneAuthenticationError as auth_err:
71  _LOGGER.error(
72  "Auth error during login with type: %s and message: %s",
73  auth_err.type,
74  auth_err.message,
75  )
76  return False
77  except CrownstoneUnknownError as unknown_err:
78  _LOGGER.error("Unknown error during login")
79  raise ConfigEntryNotReady from unknown_err
80 
81  # A new clientsession is created because the default one does not cleanup on unload
82  self.ssesse = CrownstoneSSEAsync(
83  email=email,
84  password=password,
85  access_token=self.cloudcloud.access_token,
86  websession=aiohttp_client.async_create_clientsession(self.hasshass),
87  project_name=PROJECT_NAME,
88  )
89  # Listen for events in the background, without task tracking
90  self.config_entryconfig_entry.async_create_background_task(
91  self.hasshass, self.async_process_eventsasync_process_events(self.ssesse), "crownstone-sse"
92  )
94 
95  # Set up a Crownstone USB only if path exists
96  if self.config_entryconfig_entry.options[CONF_USB_PATH] is not None:
97  await self.async_setup_usbasync_setup_usb()
98 
99  # Save the sphere where the USB is located
100  # Makes HA aware of the Crownstone environment HA is placed in, a user can have multiple
101  self.usb_sphere_idusb_sphere_id = self.config_entryconfig_entry.options[CONF_USB_SPHERE]
102 
103  await self.hasshass.config_entries.async_forward_entry_setups(
104  self.config_entryconfig_entry, PLATFORMS
105  )
106 
107  # HA specific listeners
108  self.config_entryconfig_entry.async_on_unload(
109  self.config_entryconfig_entry.add_update_listener(_async_update_listener)
110  )
111  self.config_entryconfig_entry.async_on_unload(
112  self.hasshass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.on_shutdownon_shutdown)
113  )
114 
115  return True
116 
117  async def async_process_events(self, sse_client: CrownstoneSSEAsync) -> None:
118  """Asynchronous iteration of Crownstone SSE events."""
119  async with sse_client as client:
120  async for event in client:
121  if event is not None:
122  async_dispatcher_send(self.hasshass, f"{DOMAIN}_{event.type}", event)
123 
124  async def async_setup_usb(self) -> None:
125  """Attempt setup of a Crownstone usb dongle."""
126  # Trace by-id symlink back to the serial port
127  serial_port = await self.hasshass.async_add_executor_job(
128  get_port, self.config_entryconfig_entry.options[CONF_USB_PATH]
129  )
130  if serial_port is None:
131  return
132 
133  self.uartuart = CrownstoneUart()
134  # UartException is raised when serial controller fails to open
135  try:
136  await self.uartuart.initialize_usb(serial_port)
137  except UartException:
138  self.uartuart = None
139  # Set entry options for usb to null
140  updated_options = self.config_entryconfig_entry.options.copy()
141  updated_options[CONF_USB_PATH] = None
142  updated_options[CONF_USB_SPHERE] = None
143  # Ensure that the user can configure an USB again from options
144  self.hasshass.config_entries.async_update_entry(
145  self.config_entryconfig_entry, options=updated_options
146  )
147  # Show notification to ensure the user knows the cloud is now used
148  persistent_notification.async_create(
149  self.hasshass,
150  (
151  "Setup of Crownstone USB dongle was unsuccessful on port"
152  f" {serial_port}.\n Crownstone Cloud will be used"
153  " to switch Crownstones.\n Please check if your"
154  " port is correct and set up the USB again from integration"
155  " options."
156  ),
157  "Crownstone",
158  "crownstone_usb_dongle_setup",
159  )
160  return
161 
163 
164  async def async_unload(self) -> bool:
165  """Unload the current config entry."""
166  # Authentication failed
167  if self.cloudcloud.cloud_data is None:
168  return True
169 
170  self.ssesse.close_client()
171  for sse_unsub in self.listeners[SSE_LISTENERS]:
172  sse_unsub()
173 
174  if self.uartuart:
175  self.uartuart.stop()
176  for subscription_id in self.listeners[UART_LISTENERS]:
177  UartEventBus.unsubscribe(subscription_id)
178 
179  unload_ok = await self.hasshass.config_entries.async_unload_platforms(
180  self.config_entryconfig_entry, PLATFORMS
181  )
182 
183  if unload_ok:
184  self.hasshass.data[DOMAIN].pop(self.config_entryconfig_entry.entry_id)
185 
186  return unload_ok
187 
188  @callback
189  def on_shutdown(self, _: Event) -> None:
190  """Close all IO connections."""
191  self.ssesse.close_client()
192  if self.uartuart:
193  self.uartuart.stop()
194 
195 
196 async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
197  """Handle options update."""
198  await hass.config_entries.async_reload(entry.entry_id)
None __init__(self, HomeAssistant hass, ConfigEntry config_entry)
None _async_update_listener(HomeAssistant hass, ConfigEntry entry)
None setup_sse_listeners(CrownstoneEntryManager manager)
Definition: listeners.py:121
None setup_uart_listeners(CrownstoneEntryManager manager)
Definition: listeners.py:138
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)
Definition: dispatcher.py:193