Move esphome coordinator to separate module (#117427)

This commit is contained in:
epenet 2024-05-14 15:16:47 +02:00 committed by GitHub
parent 92bb76ed24
commit 2e155f4de5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 84 additions and 71 deletions

View File

@ -0,0 +1,57 @@
"""Coordinator to interact with an ESPHome dashboard."""
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.update_coordinator import DataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
MIN_VERSION_SUPPORTS_UPDATE = AwesomeVersion("2023.1.0")
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."""
super().__init__(
hass,
_LOGGER,
name="ESPHome Dashboard",
update_interval=timedelta(minutes=5),
always_update=False,
)
self.addon_slug = addon_slug
self.url = url
self.api = ESPHomeDashboardAPI(url, session)
self.supports_update: bool | None = None
async def _async_update_data(self) -> dict:
"""Fetch device data."""
devices = await self.api.get_devices()
configured_devices = devices["configured"]
if (
self.supports_update is None
and configured_devices
and (current_version := configured_devices[0].get("current_version"))
):
self.supports_update = (
AwesomeVersion(current_version) > MIN_VERSION_SUPPORTS_UPDATE
)
return {dev["name"]: dev for dev in configured_devices}

View File

@ -1,25 +1,20 @@
"""Files to interact with a the ESPHome dashboard.""" """Files to interact with an ESPHome dashboard."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
from datetime import timedelta
import logging import logging
from typing import Any from typing import Any
import aiohttp
from awesomeversion import AwesomeVersion
from esphome_dashboard_api import ConfiguredDevice, ESPHomeDashboardAPI
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback from homeassistant.core import CALLBACK_TYPE, Event, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.singleton import singleton from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN from .const import DOMAIN
from .coordinator import ESPHomeDashboardCoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -29,8 +24,6 @@ KEY_DASHBOARD_MANAGER = "esphome_dashboard_manager"
STORAGE_KEY = "esphome.dashboard" STORAGE_KEY = "esphome.dashboard"
STORAGE_VERSION = 1 STORAGE_VERSION = 1
MIN_VERSION_SUPPORTS_UPDATE = AwesomeVersion("2023.1.0")
async def async_setup(hass: HomeAssistant) -> None: async def async_setup(hass: HomeAssistant) -> None:
"""Set up the ESPHome dashboard.""" """Set up the ESPHome dashboard."""
@ -58,7 +51,7 @@ class ESPHomeDashboardManager:
self._hass = hass self._hass = hass
self._store: Store[dict[str, Any]] = Store(hass, STORAGE_VERSION, STORAGE_KEY) self._store: Store[dict[str, Any]] = Store(hass, STORAGE_VERSION, STORAGE_KEY)
self._data: dict[str, Any] | None = None self._data: dict[str, Any] | None = None
self._current_dashboard: ESPHomeDashboard | None = None self._current_dashboard: ESPHomeDashboardCoordinator | None = None
self._cancel_shutdown: CALLBACK_TYPE | None = None self._cancel_shutdown: CALLBACK_TYPE | None = None
async def async_setup(self) -> None: async def async_setup(self) -> None:
@ -70,7 +63,7 @@ class ESPHomeDashboardManager:
) )
@callback @callback
def async_get(self) -> ESPHomeDashboard | None: def async_get(self) -> ESPHomeDashboardCoordinator | None:
"""Get the current dashboard.""" """Get the current dashboard."""
return self._current_dashboard return self._current_dashboard
@ -92,7 +85,7 @@ class ESPHomeDashboardManager:
self._cancel_shutdown = None self._cancel_shutdown = None
self._current_dashboard = None self._current_dashboard = None
dashboard = ESPHomeDashboard( dashboard = ESPHomeDashboardCoordinator(
hass, addon_slug, url, async_get_clientsession(hass) hass, addon_slug, url, async_get_clientsession(hass)
) )
await dashboard.async_request_refresh() await dashboard.async_request_refresh()
@ -138,7 +131,7 @@ class ESPHomeDashboardManager:
@callback @callback
def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboard | None: def async_get_dashboard(hass: HomeAssistant) -> ESPHomeDashboardCoordinator | None:
"""Get an instance of the dashboard if set. """Get an instance of the dashboard if set.
This is only safe to call after `async_setup` has been completed. This is only safe to call after `async_setup` has been completed.
@ -157,43 +150,3 @@ async def async_set_dashboard_info(
"""Set the dashboard info.""" """Set the dashboard info."""
manager = await async_get_or_create_dashboard_manager(hass) manager = await async_get_or_create_dashboard_manager(hass)
await manager.async_set_dashboard_info(addon_slug, host, port) await manager.async_set_dashboard_info(addon_slug, host, port)
class ESPHomeDashboard(DataUpdateCoordinator[dict[str, ConfiguredDevice]]): # pylint: disable=hass-enforce-coordinator-module
"""Class to interact with the ESPHome dashboard."""
def __init__(
self,
hass: HomeAssistant,
addon_slug: str,
url: str,
session: aiohttp.ClientSession,
) -> None:
"""Initialize."""
super().__init__(
hass,
_LOGGER,
name="ESPHome Dashboard",
update_interval=timedelta(minutes=5),
always_update=False,
)
self.addon_slug = addon_slug
self.url = url
self.api = ESPHomeDashboardAPI(url, session)
self.supports_update: bool | None = None
async def _async_update_data(self) -> dict:
"""Fetch device data."""
devices = await self.api.get_devices()
configured_devices = devices["configured"]
if (
self.supports_update is None
and configured_devices
and (current_version := configured_devices[0].get("current_version"))
):
self.supports_update = (
AwesomeVersion(current_version) > MIN_VERSION_SUPPORTS_UPDATE
)
return {dev["name"]: dev for dev in configured_devices}

View File

@ -20,7 +20,8 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .dashboard import ESPHomeDashboard, async_get_dashboard from .coordinator import ESPHomeDashboardCoordinator
from .dashboard import async_get_dashboard
from .domain_data import DomainData from .domain_data import DomainData
from .entry_data import RuntimeEntryData from .entry_data import RuntimeEntryData
@ -65,7 +66,7 @@ async def async_setup_entry(
] ]
class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity): class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboardCoordinator], UpdateEntity):
"""Defines an ESPHome update entity.""" """Defines an ESPHome update entity."""
_attr_has_entity_name = True _attr_has_entity_name = True
@ -75,7 +76,7 @@ class ESPHomeUpdateEntity(CoordinatorEntity[ESPHomeDashboard], UpdateEntity):
_attr_release_url = "https://esphome.io/changelog/" _attr_release_url = "https://esphome.io/changelog/"
def __init__( def __init__(
self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboard self, entry_data: RuntimeEntryData, coordinator: ESPHomeDashboardCoordinator
) -> None: ) -> None:
"""Initialize the update entity.""" """Initialize the update entity."""
super().__init__(coordinator=coordinator) super().__init__(coordinator=coordinator)

View File

@ -338,7 +338,7 @@ async def test_user_dashboard_has_wrong_key(
] ]
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=WRONG_NOISE_PSK, return_value=WRONG_NOISE_PSK,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -393,7 +393,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard(
await dashboard.async_get_dashboard(hass).async_refresh() await dashboard.async_get_dashboard(hass).async_refresh()
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -446,7 +446,7 @@ async def test_user_discovers_name_and_gets_key_from_dashboard_fails(
await dashboard.async_get_dashboard(hass).async_refresh() await dashboard.async_get_dashboard(hass).async_refresh()
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
side_effect=dashboard_exception, side_effect=dashboard_exception,
): ):
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -859,7 +859,7 @@ async def test_reauth_fixed_via_dashboard(
await dashboard.async_get_dashboard(hass).async_refresh() await dashboard.async_get_dashboard(hass).async_refresh()
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key: ) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -902,7 +902,7 @@ async def test_reauth_fixed_via_dashboard_add_encryption_remove_password(
await dashboard.async_get_dashboard(hass).async_refresh() await dashboard.async_get_dashboard(hass).async_refresh()
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key: ) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -990,7 +990,7 @@ async def test_reauth_fixed_via_dashboard_at_confirm(
await dashboard.async_get_dashboard(hass).async_refresh() await dashboard.async_get_dashboard(hass).async_refresh()
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key: ) as mock_get_encryption_key:
# We just fetch the form # We just fetch the form
@ -1211,7 +1211,7 @@ async def test_zeroconf_encryption_key_via_dashboard(
] ]
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key: ) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(
@ -1277,7 +1277,7 @@ async def test_zeroconf_encryption_key_via_dashboard_with_api_encryption_prop(
] ]
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key: ) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_configure( result = await hass.config_entries.flow.async_configure(

View File

@ -4,7 +4,7 @@ from unittest.mock import patch
from aioesphomeapi import DeviceInfo, InvalidAuthAPIError from aioesphomeapi import DeviceInfo, InvalidAuthAPIError
from homeassistant.components.esphome import CONF_NOISE_PSK, dashboard from homeassistant.components.esphome import CONF_NOISE_PSK, coordinator, dashboard
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -56,7 +56,7 @@ async def test_restore_dashboard_storage_end_to_end(
"data": {"info": {"addon_slug": "test-slug", "host": "new-host", "port": 6052}}, "data": {"info": {"addon_slug": "test-slug", "host": "new-host", "port": 6052}},
} }
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI" "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI"
) as mock_dashboard_api: ) as mock_dashboard_api:
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -69,7 +69,7 @@ async def test_setup_dashboard_fails(
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Test that nothing is stored on failed dashboard setup when there was no dashboard before.""" """Test that nothing is stored on failed dashboard setup when there was no dashboard before."""
with patch.object( with patch.object(
dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError coordinator.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError
) as mock_get_devices: ) as mock_get_devices:
await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@ -86,7 +86,9 @@ async def test_setup_dashboard_fails_when_already_setup(
hass: HomeAssistant, mock_config_entry: MockConfigEntry, hass_storage hass: HomeAssistant, mock_config_entry: MockConfigEntry, hass_storage
) -> MockConfigEntry: ) -> MockConfigEntry:
"""Test failed dashboard setup still reloads entries if one existed before.""" """Test failed dashboard setup still reloads entries if one existed before."""
with patch.object(dashboard.ESPHomeDashboardAPI, "get_devices") as mock_get_devices: with patch.object(
coordinator.ESPHomeDashboardAPI, "get_devices"
) as mock_get_devices:
await dashboard.async_set_dashboard_info( await dashboard.async_set_dashboard_info(
hass, "test-slug", "working-host", 6052 hass, "test-slug", "working-host", 6052
) )
@ -100,7 +102,7 @@ async def test_setup_dashboard_fails_when_already_setup(
with ( with (
patch.object( patch.object(
dashboard.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError coordinator.ESPHomeDashboardAPI, "get_devices", side_effect=TimeoutError
) as mock_get_devices, ) as mock_get_devices,
patch( patch(
"homeassistant.components.esphome.async_setup_entry", return_value=True "homeassistant.components.esphome.async_setup_entry", return_value=True
@ -145,7 +147,7 @@ async def test_new_dashboard_fix_reauth(
) )
with patch( with patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key: ) as mock_get_encryption_key:
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
@ -171,7 +173,7 @@ async def test_new_dashboard_fix_reauth(
with ( with (
patch( patch(
"homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key", "homeassistant.components.esphome.coordinator.ESPHomeDashboardAPI.get_encryption_key",
return_value=VALID_NOISE_PSK, return_value=VALID_NOISE_PSK,
) as mock_get_encryption_key, ) as mock_get_encryption_key,
patch( patch(