core/homeassistant/components/crownstone/entry_manager.py

199 lines
6.9 KiB
Python

"""Manager to set up IO with Crownstone devices for a config entry."""
from __future__ import annotations
import logging
from typing import Any
from crownstone_cloud import CrownstoneCloud
from crownstone_cloud.exceptions import (
CrownstoneAuthenticationError,
CrownstoneUnknownError,
)
from crownstone_sse import CrownstoneSSEAsync
from crownstone_uart import CrownstoneUart, UartEventBus
from crownstone_uart.Exceptions import UartException
from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import async_dispatcher_send
from .const import (
CONF_USB_PATH,
CONF_USB_SPHERE,
DOMAIN,
PLATFORMS,
PROJECT_NAME,
SSE_LISTENERS,
UART_LISTENERS,
)
from .helpers import get_port
from .listeners import setup_sse_listeners, setup_uart_listeners
_LOGGER = logging.getLogger(__name__)
class CrownstoneEntryManager:
"""Manage a Crownstone config entry."""
uart: CrownstoneUart | None = None
cloud: CrownstoneCloud
sse: CrownstoneSSEAsync
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize the hub."""
self.hass = hass
self.config_entry = config_entry
self.listeners: dict[str, Any] = {}
self.usb_sphere_id: str | None = None
async def async_setup(self) -> bool:
"""Set up a Crownstone config entry.
Returns True if the setup was successful.
"""
email = self.config_entry.data[CONF_EMAIL]
password = self.config_entry.data[CONF_PASSWORD]
self.cloud = CrownstoneCloud(
email=email,
password=password,
clientsession=aiohttp_client.async_get_clientsession(self.hass),
)
# Login & sync all user data
try:
await self.cloud.async_initialize()
except CrownstoneAuthenticationError as auth_err:
_LOGGER.error(
"Auth error during login with type: %s and message: %s",
auth_err.type,
auth_err.message,
)
return False
except CrownstoneUnknownError as unknown_err:
_LOGGER.error("Unknown error during login")
raise ConfigEntryNotReady from unknown_err
# A new clientsession is created because the default one does not cleanup on unload
self.sse = CrownstoneSSEAsync(
email=email,
password=password,
access_token=self.cloud.access_token,
websession=aiohttp_client.async_create_clientsession(self.hass),
project_name=PROJECT_NAME,
)
# Listen for events in the background, without task tracking
self.config_entry.async_create_background_task(
self.hass, self.async_process_events(self.sse), "crownstone-sse"
)
setup_sse_listeners(self)
# Set up a Crownstone USB only if path exists
if self.config_entry.options[CONF_USB_PATH] is not None:
await self.async_setup_usb()
# Save the sphere where the USB is located
# Makes HA aware of the Crownstone environment HA is placed in, a user can have multiple
self.usb_sphere_id = self.config_entry.options[CONF_USB_SPHERE]
await self.hass.config_entries.async_forward_entry_setups(
self.config_entry, PLATFORMS
)
# HA specific listeners
self.config_entry.async_on_unload(
self.config_entry.add_update_listener(_async_update_listener)
)
self.config_entry.async_on_unload(
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.on_shutdown)
)
return True
async def async_process_events(self, sse_client: CrownstoneSSEAsync) -> None:
"""Asynchronous iteration of Crownstone SSE events."""
async with sse_client as client:
async for event in client:
if event is not None:
async_dispatcher_send(self.hass, f"{DOMAIN}_{event.type}", event)
async def async_setup_usb(self) -> None:
"""Attempt setup of a Crownstone usb dongle."""
# Trace by-id symlink back to the serial port
serial_port = await self.hass.async_add_executor_job(
get_port, self.config_entry.options[CONF_USB_PATH]
)
if serial_port is None:
return
self.uart = CrownstoneUart()
# UartException is raised when serial controller fails to open
try:
await self.uart.initialize_usb(serial_port)
except UartException:
self.uart = None
# Set entry options for usb to null
updated_options = self.config_entry.options.copy()
updated_options[CONF_USB_PATH] = None
updated_options[CONF_USB_SPHERE] = None
# Ensure that the user can configure an USB again from options
self.hass.config_entries.async_update_entry(
self.config_entry, options=updated_options
)
# Show notification to ensure the user knows the cloud is now used
persistent_notification.async_create(
self.hass,
(
"Setup of Crownstone USB dongle was unsuccessful on port"
f" {serial_port}.\n Crownstone Cloud will be used"
" to switch Crownstones.\n Please check if your"
" port is correct and set up the USB again from integration"
" options."
),
"Crownstone",
"crownstone_usb_dongle_setup",
)
return
setup_uart_listeners(self)
async def async_unload(self) -> bool:
"""Unload the current config entry."""
# Authentication failed
if self.cloud.cloud_data is None:
return True
self.sse.close_client()
for sse_unsub in self.listeners[SSE_LISTENERS]:
sse_unsub()
if self.uart:
self.uart.stop()
for subscription_id in self.listeners[UART_LISTENERS]:
UartEventBus.unsubscribe(subscription_id)
unload_ok = await self.hass.config_entries.async_unload_platforms(
self.config_entry, PLATFORMS
)
if unload_ok:
self.hass.data[DOMAIN].pop(self.config_entry.entry_id)
return unload_ok
@callback
def on_shutdown(self, _: Event) -> None:
"""Close all IO connections."""
self.sse.close_client()
if self.uart:
self.uart.stop()
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)