mirror of https://github.com/home-assistant/core
199 lines
6.9 KiB
Python
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)
|