Quality improvements for the ESPHome dashboard coordinator (#143619)

This commit is contained in:
J. Nick Koston 2025-04-24 11:20:05 -10:00 committed by GitHub
parent 2abe2f7d59
commit fab70a80bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 98 additions and 35 deletions

View File

@ -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"]

View File

@ -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

View File

@ -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(

View File

@ -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