diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 41b1b780a1..0cd8804138 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -56,10 +56,11 @@ from homeassistant.helpers.issue_registry import ( ) from homeassistant.helpers.service import async_set_service_schema from homeassistant.helpers.template import Template +from homeassistant.helpers.typing import ConfigType from .bluetooth import async_connect_scanner from .const import DOMAIN -from .dashboard import async_get_dashboard +from .dashboard import async_get_dashboard, async_setup as async_setup_dashboard from .domain_data import DomainData # Import config flow so that it's added to the registry @@ -135,6 +136,12 @@ def _async_check_using_api_password( ) +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the esphome component.""" + await async_setup_dashboard(hass) + return True + + async def async_setup_entry( # noqa: C901 hass: HomeAssistant, entry: ConfigEntry ) -> bool: diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index a8332f8d04..9c926a334c 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -4,6 +4,7 @@ from __future__ import annotations import asyncio from datetime import timedelta import logging +from typing import Any import aiohttp from awesomeversion import AwesomeVersion @@ -11,62 +12,138 @@ from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN -KEY_DASHBOARD = "esphome_dashboard" +KEY_DASHBOARD_MANAGER = "esphome_dashboard_manager" + +STORAGE_KEY = "esphome.dashboard" +STORAGE_VERSION = 1 + + +async def async_setup(hass: HomeAssistant) -> None: + """Set up the ESPHome dashboard.""" + await async_get_or_create_dashboard_manager(hass) + + +@callback +def async_get_dashboard_manager(hass: HomeAssistant) -> ESPHomeDashboardManager | None: + """Get the dashboard manager if it already exists.""" + manager: ESPHomeDashboardManager | None = hass.data.get(KEY_DASHBOARD_MANAGER) + return manager + + +async def async_get_or_create_dashboard_manager( + hass: HomeAssistant, +) -> ESPHomeDashboardManager: + """Get the dashboard manager or create it.""" + if KEY_DASHBOARD_MANAGER not in hass.data: + manager = ESPHomeDashboardManager(hass) + await manager.async_setup() + hass.data[KEY_DASHBOARD_MANAGER] = manager + else: + manager = hass.data[KEY_DASHBOARD_MANAGER] + return manager + + +class ESPHomeDashboardManager: + """Class to manage the dashboard and restore it from storage.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the dashboard manager.""" + self.hass = hass + self.store: Store[dict[str, Any]] = Store(hass, STORAGE_VERSION, STORAGE_KEY) + self.data: dict[str, Any] | None = None + self.current_dashboard: ESPHomeDashboard | None = None + self._cancel_shutdown: CALLBACK_TYPE | None = None + + async def async_setup(self) -> None: + """Restore the dashboard from storage.""" + self.data = await self.store.async_load() + if data := self.data: + await self.async_set_dashboard_info( + data["addon_slug"], data["host"], data["port"] + ) + + @callback + def async_get(self) -> ESPHomeDashboard | None: + """Get the current dashboard.""" + return self.current_dashboard + + async def async_set_dashboard_info( + self, addon_slug: str, host: str, port: int + ) -> None: + """Set the dashboard info.""" + url = f"http://{host}:{port}" + hass = self.hass + cur_dashboard = self.current_dashboard + + if cur_dashboard is not None: + if cur_dashboard.addon_slug == addon_slug and cur_dashboard.url == url: + # Do nothing if we already have this data. + return + # Clear and make way for new dashboard + await cur_dashboard.async_shutdown() + if self._cancel_shutdown is not None: + self._cancel_shutdown() + self._cancel_shutdown = None + self.current_dashboard = None + + dashboard = ESPHomeDashboard( + hass, addon_slug, url, async_get_clientsession(hass) + ) + try: + await dashboard.async_request_refresh() + except UpdateFailed as err: + logging.getLogger(__name__).error("Ignoring dashboard info: %s", err) + return + + self.current_dashboard = dashboard + + async def on_hass_stop(_: Event) -> None: + await dashboard.async_shutdown() + + self._cancel_shutdown = hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, on_hass_stop + ) + + new_data = {"addon_slug": addon_slug, "host": host, "port": port} + if self.data != new_data: + await self.store.async_save(new_data) + + reloads = [ + hass.config_entries.async_reload(entry.entry_id) + for entry in hass.config_entries.async_entries(DOMAIN) + if entry.state == ConfigEntryState.LOADED + ] + # Re-auth flows will check the dashboard for encryption key when the form is requested + reauths = [ + hass.config_entries.flow.async_configure(flow["flow_id"]) + for flow in hass.config_entries.flow.async_progress() + if flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH + ] + if reloads or reauths: + await asyncio.gather(*reloads, *reauths) @callback def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: """Get an instance of the dashboard if set.""" - return hass.data.get(KEY_DASHBOARD) + if manager := async_get_dashboard_manager(hass): + return manager.async_get() + return None async def async_set_dashboard_info( hass: HomeAssistant, addon_slug: str, host: str, port: int ) -> None: """Set the dashboard info.""" - url = f"http://{host}:{port}" - - if cur_dashboard := async_get_dashboard(hass): - if cur_dashboard.addon_slug == addon_slug and cur_dashboard.url == url: - # Do nothing if we already have this data. - return - # Clear and make way for new dashboard - await cur_dashboard.async_shutdown() - del hass.data[KEY_DASHBOARD] - - dashboard = ESPHomeDashboard(hass, addon_slug, url, async_get_clientsession(hass)) - try: - await dashboard.async_request_refresh() - except UpdateFailed as err: - logging.getLogger(__name__).error("Ignoring dashboard info: %s", err) - return - - hass.data[KEY_DASHBOARD] = dashboard - - async def on_hass_stop(_: Event) -> None: - await dashboard.async_shutdown() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop) - - reloads = [ - hass.config_entries.async_reload(entry.entry_id) - for entry in hass.config_entries.async_entries(DOMAIN) - if entry.state == ConfigEntryState.LOADED - ] - # Re-auth flows will check the dashboard for encryption key when the form is requested - reauths = [ - hass.config_entries.flow.async_configure(flow["flow_id"]) - for flow in hass.config_entries.flow.async_progress() - if flow["handler"] == DOMAIN and flow["context"]["source"] == SOURCE_REAUTH - ] - if reloads or reauths: - await asyncio.gather(*reloads, *reauths) + manager = await async_get_or_create_dashboard_manager(hass) + await manager.async_set_dashboard_info(addon_slug, host, port) class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]):