diff --git a/homeassistant/components/esphome/coordinator.py b/homeassistant/components/esphome/coordinator.py index b31a74dcf3f..99ae6d38a9d 100644 --- a/homeassistant/components/esphome/coordinator.py +++ b/homeassistant/components/esphome/coordinator.py @@ -5,43 +5,38 @@ from __future__ import annotations from datetime import timedelta import logging -import aiohttp from awesomeversion import AwesomeVersion from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI from homeassistant.core import HomeAssistant +from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator _LOGGER = logging.getLogger(__name__) MIN_VERSION_SUPPORTS_UPDATE = AwesomeVersion("2023.1.0") +REFRESH_INTERVAL = timedelta(minutes=5) class ESPHomeDashboardCoordinator(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): """Class to interact with the ESPHome dashboard.""" - def __init__( - self, - hass: HomeAssistant, - addon_slug: str, - url: str, - session: aiohttp.ClientSession, - ) -> None: - """Initialize.""" + def __init__(self, hass: HomeAssistant, addon_slug: str, url: str) -> None: + """Initialize the dashboard coordinator.""" super().__init__( hass, _LOGGER, config_entry=None, name="ESPHome Dashboard", - update_interval=timedelta(minutes=5), + update_interval=REFRESH_INTERVAL, always_update=False, ) self.addon_slug = addon_slug self.url = url - self.api = ESPHomeDashboardAPI(url, session) + self.api = ESPHomeDashboardAPI(url, async_get_clientsession(hass)) self.supports_update: bool | None = None - async def _async_update_data(self) -> dict: + async def _async_update_data(self) -> dict[str, ConfiguredDevice]: """Fetch device data.""" devices = await self.api.get_devices() configured_devices = devices["configured"] diff --git a/homeassistant/components/esphome/dashboard.py b/homeassistant/components/esphome/dashboard.py index bbe4698f278..5f879edf005 100644 --- a/homeassistant/components/esphome/dashboard.py +++ b/homeassistant/components/esphome/dashboard.py @@ -9,7 +9,6 @@ from typing import Any from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback -from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.hassio import is_hassio from homeassistant.helpers.singleton import singleton from homeassistant.helpers.storage import Store @@ -104,9 +103,7 @@ class ESPHomeDashboardManager: self._cancel_shutdown = None self._current_dashboard = None - dashboard = ESPHomeDashboardCoordinator( - hass, addon_slug, url, async_get_clientsession(hass) - ) + dashboard = ESPHomeDashboardCoordinator(hass, addon_slug, url) await dashboard.async_request_refresh() self._current_dashboard = dashboard diff --git a/homeassistant/components/esphome/update.py b/homeassistant/components/esphome/update.py index d2c8d9dc3d0..a92204a80d2 100644 --- a/homeassistant/components/esphome/update.py +++ b/homeassistant/components/esphome/update.py @@ -70,7 +70,6 @@ async def async_setup_entry( @callback def _async_setup_update_entity() -> None: """Set up the update entity.""" - nonlocal unsubs assert dashboard is not None # Keep listening until device is available if not entry_data.available or not dashboard.last_update_success: @@ -95,10 +94,12 @@ async def async_setup_entry( _async_setup_update_entity() return - unsubs = [ - entry_data.async_subscribe_device_updated(_async_setup_update_entity), - dashboard.async_add_listener(_async_setup_update_entity), - ] + unsubs.extend( + [ + entry_data.async_subscribe_device_updated(_async_setup_update_entity), + dashboard.async_add_listener(_async_setup_update_entity), + ] + ) class ESPHomeDashboardUpdateEntity( diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index 5fa53dc7f75..99bdd5b5f47 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,26 +1,46 @@ """Test ESPHome dashboard features.""" +from datetime import datetime from typing import Any from unittest.mock import patch -from aioesphomeapi import DeviceInfo, InvalidAuthAPIError +from aioesphomeapi import APIClient, DeviceInfo, InvalidAuthAPIError import pytest from homeassistant.components.esphome import CONF_NOISE_PSK, DOMAIN, dashboard +from homeassistant.components.esphome.coordinator import REFRESH_INTERVAL from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from . import VALID_NOISE_PSK +from .conftest import MockESPHomeDeviceType -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed +class MockDashboardRefresh: + """Mock dashboard refresh.""" + + def __init__(self, hass: HomeAssistant) -> None: + """Initialize the mock dashboard refresh.""" + self.hass = hass + self.last_time: datetime | None = None + + async def async_refresh(self) -> None: + """Refresh the dashboard.""" + if self.last_time is None: + self.last_time = dt_util.utcnow() + self.last_time += REFRESH_INTERVAL + async_fire_time_changed(self.hass, self.last_time) + await self.hass.async_block_till_done() + + +@pytest.mark.usefixtures("init_integration", "mock_dashboard") async def test_dashboard_storage( hass: HomeAssistant, - init_integration, - mock_dashboard: dict[str, Any], hass_storage: dict[str, Any], ) -> None: """Test dashboard storage.""" @@ -165,8 +185,9 @@ async def test_setup_dashboard_fails_when_already_setup( assert len(mock_setup.mock_calls) == 1 +@pytest.mark.usefixtures("mock_dashboard") async def test_new_info_reload_config_entries( - hass: HomeAssistant, init_integration, mock_dashboard + hass: HomeAssistant, init_integration: MockConfigEntry ) -> None: """Test config entries are reloaded when new info is set.""" assert init_integration.state is ConfigEntryState.LOADED @@ -185,7 +206,10 @@ async def test_new_info_reload_config_entries( async def test_new_dashboard_fix_reauth( - hass: HomeAssistant, mock_client, mock_config_entry: MockConfigEntry, mock_dashboard + hass: HomeAssistant, + mock_client: APIClient, + mock_config_entry: MockConfigEntry, + mock_dashboard: dict[str, Any], ) -> None: """Test config entries waiting for reauth are triggered.""" mock_client.device_info.side_effect = ( @@ -209,7 +233,7 @@ async def test_new_dashboard_fix_reauth( } ) - await dashboard.async_get_dashboard(hass).async_refresh() + await MockDashboardRefresh(hass).async_refresh() with ( patch( @@ -229,15 +253,29 @@ async def test_new_dashboard_fix_reauth( async def test_dashboard_supports_update( - hass: HomeAssistant, mock_dashboard: dict[str, Any] + hass: HomeAssistant, + mock_dashboard: dict[str, Any], + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, ) -> None: """Test dashboard supports update.""" dash = dashboard.async_get_dashboard(hass) + mock_refresh = MockDashboardRefresh(hass) + + entity_info = [] + states = [] + user_service = [] + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) # No data assert not dash.supports_update - await dash.async_refresh() + await mock_refresh.async_refresh() assert dash.supports_update is None # supported version @@ -248,12 +286,44 @@ async def test_dashboard_supports_update( "current_version": "2023.2.0-dev", } ) - await dash.async_refresh() + + await mock_refresh.async_refresh() assert dash.supports_update is True - # unsupported version - dash.supports_update = None - mock_dashboard["configured"][0]["current_version"] = "2023.1.0" - await dash.async_refresh() +async def test_dashboard_unsupported_version( + hass: HomeAssistant, + mock_dashboard: dict[str, Any], + mock_client: APIClient, + mock_esphome_device: MockESPHomeDeviceType, +) -> None: + """Test dashboard with unsupported version.""" + dash = dashboard.async_get_dashboard(hass) + mock_refresh = MockDashboardRefresh(hass) + + entity_info = [] + states = [] + user_service = [] + await mock_esphome_device( + mock_client=mock_client, + entity_info=entity_info, + user_service=user_service, + states=states, + ) + + # No data + assert not dash.supports_update + + await mock_refresh.async_refresh() + assert dash.supports_update is None + + # unsupported version + mock_dashboard["configured"].append( + { + "name": "test", + "configuration": "test.yaml", + "current_version": "2023.1.0", + } + ) + await mock_refresh.async_refresh() assert dash.supports_update is False