"""Config helpers for Alexa.""" from __future__ import annotations from abc import ABC, abstractmethod import asyncio import logging from typing import Any from yarl import URL from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.storage import Store from .const import DOMAIN from .state_report import async_enable_proactive_mode STORE_AUTHORIZED = "authorized" _LOGGER = logging.getLogger(__name__) class AbstractConfig(ABC): """Hold the configuration for Alexa.""" _store: AlexaConfigStore _unsub_proactive_report: CALLBACK_TYPE | None = None def __init__(self, hass: HomeAssistant) -> None: """Initialize abstract config.""" self.hass = hass self._enable_proactive_mode_lock = asyncio.Lock() async def async_initialize(self) -> None: """Perform async initialization of config.""" self._store = AlexaConfigStore(self.hass) await self._store.async_load() @property def supports_auth(self) -> bool: """Return if config supports auth.""" return False @property def should_report_state(self) -> bool: """Return if states should be proactively reported.""" return False @property @abstractmethod def endpoint(self) -> str | URL | None: """Endpoint for report state.""" @property @abstractmethod def locale(self) -> str | None: """Return config locale.""" @property def entity_config(self) -> dict[str, Any]: """Return entity config.""" return {} @property def is_reporting_states(self) -> bool: """Return if proactive mode is enabled.""" return self._unsub_proactive_report is not None @callback @abstractmethod def user_identifier(self) -> str: """Return an identifier for the user that represents this config.""" async def async_enable_proactive_mode(self) -> None: """Enable proactive mode.""" _LOGGER.debug("Enable proactive mode") async with self._enable_proactive_mode_lock: if self._unsub_proactive_report is not None: return self._unsub_proactive_report = await async_enable_proactive_mode( self.hass, self ) async def async_disable_proactive_mode(self) -> None: """Disable proactive mode.""" _LOGGER.debug("Disable proactive mode") if unsub_func := self._unsub_proactive_report: unsub_func() self._unsub_proactive_report = None @callback def should_expose(self, entity_id: str) -> bool: """If an entity should be exposed.""" return False @callback def async_invalidate_access_token(self) -> None: """Invalidate access token.""" raise NotImplementedError async def async_get_access_token(self) -> str | None: """Get an access token.""" raise NotImplementedError async def async_accept_grant(self, code: str) -> str | None: """Accept a grant.""" raise NotImplementedError @property def authorized(self) -> bool: """Return authorization status.""" return self._store.authorized async def set_authorized(self, authorized: bool) -> None: """Set authorization status. - Set when an incoming message is received from Alexa. - Unset if state reporting fails """ self._store.set_authorized(authorized) if self.should_report_state != self.is_reporting_states: if self.should_report_state: try: await self.async_enable_proactive_mode() except Exception: # We failed to enable proactive mode, unset authorized flag self._store.set_authorized(False) raise else: await self.async_disable_proactive_mode() class AlexaConfigStore: """A configuration store for Alexa.""" _STORAGE_VERSION = 1 _STORAGE_KEY = DOMAIN def __init__(self, hass: HomeAssistant) -> None: """Initialize a configuration store.""" self._data: dict[str, Any] | None = None self._hass = hass self._store: Store = Store(hass, self._STORAGE_VERSION, self._STORAGE_KEY) @property def authorized(self) -> bool: """Return authorization status.""" assert self._data is not None return bool(self._data[STORE_AUTHORIZED]) @callback def set_authorized(self, authorized: bool) -> None: """Set authorization status.""" if self._data is not None and authorized != self._data[STORE_AUTHORIZED]: self._data[STORE_AUTHORIZED] = authorized self._store.async_delay_save(lambda: self._data, 1.0) async def async_load(self) -> None: """Load saved configuration from disk.""" if data := await self._store.async_load(): self._data = data else: self._data = {STORE_AUTHORIZED: False}