From add9f4c5ab167a97a31d83ce4975d51a6b03f303 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 16 Jun 2025 14:48:44 +0200 Subject: [PATCH] Move Meater coordinator to module (#146946) * Move Meater coordinator to module * Fix tests --- homeassistant/components/meater/__init__.py | 68 +---------------- .../components/meater/coordinator.py | 75 +++++++++++++++++++ homeassistant/components/meater/sensor.py | 14 +--- tests/components/meater/test_config_flow.py | 4 +- 4 files changed, 86 insertions(+), 75 deletions(-) create mode 100644 homeassistant/components/meater/coordinator.py diff --git a/homeassistant/components/meater/__init__.py b/homeassistant/components/meater/__init__.py index 50eff40c0e8..3d4f45206b6 100644 --- a/homeassistant/components/meater/__init__.py +++ b/homeassistant/components/meater/__init__.py @@ -1,85 +1,25 @@ """The Meater Temperature Probe integration.""" -import asyncio -from datetime import timedelta -import logging - -from meater import ( - AuthenticationError, - MeaterApi, - ServiceUnavailableError, - TooManyRequestsError, -) -from meater.MeaterApi import MeaterProbe - from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN +from .coordinator import MeaterCoordinator PLATFORMS = [Platform.SENSOR] -_LOGGER = logging.getLogger(__name__) - async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Meater Temperature Probe from a config entry.""" - # Store an API object to access - session = async_get_clientsession(hass) - meater_api = MeaterApi(session) - # Add the credentials - try: - _LOGGER.debug("Authenticating with the Meater API") - await meater_api.authenticate( - entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] - ) - except (ServiceUnavailableError, TooManyRequestsError) as err: - raise ConfigEntryNotReady from err - except AuthenticationError as err: - raise ConfigEntryAuthFailed( - f"Unable to authenticate with the Meater API: {err}" - ) from err - - async def async_update_data() -> dict[str, MeaterProbe]: - """Fetch data from API endpoint.""" - try: - # Note: TimeoutError and aiohttp.ClientError are already - # handled by the data update coordinator. - async with asyncio.timeout(10): - devices: list[MeaterProbe] = await meater_api.get_all_devices() - except AuthenticationError as err: - raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err - except TooManyRequestsError as err: - raise UpdateFailed( - "Too many requests have been made to the API, rate limiting is in place" - ) from err - - return {device.id: device for device in devices} - - coordinator = DataUpdateCoordinator( - hass, - _LOGGER, - config_entry=entry, - # Name of the data. For logging purposes. - name="meater_api", - update_method=async_update_data, - # Polling interval. Will only be polled if there are subscribers. - update_interval=timedelta(seconds=30), - ) + coordinator = MeaterCoordinator(hass, entry) await coordinator.async_config_entry_first_refresh() hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN].setdefault("known_probes", set()) - hass.data[DOMAIN][entry.entry_id] = { - "api": meater_api, - "coordinator": coordinator, - } + hass.data[DOMAIN][entry.entry_id] = coordinator await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/meater/coordinator.py b/homeassistant/components/meater/coordinator.py new file mode 100644 index 00000000000..7fb0c69d4eb --- /dev/null +++ b/homeassistant/components/meater/coordinator.py @@ -0,0 +1,75 @@ +"""Meater Coordinator.""" + +import asyncio +from datetime import timedelta +import logging + +from meater.MeaterApi import ( + AuthenticationError, + MeaterApi, + MeaterProbe, + ServiceUnavailableError, + TooManyRequestsError, +) + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +_LOGGER = logging.getLogger(__name__) + + +class MeaterCoordinator(DataUpdateCoordinator[dict[str, MeaterProbe]]): + """Meater Coordinator.""" + + config_entry: ConfigEntry + + def __init__( + self, + hass: HomeAssistant, + entry: ConfigEntry, + ) -> None: + """Initialize the Meater Coordinator.""" + super().__init__( + hass, + _LOGGER, + config_entry=entry, + name=f"Meater {entry.title}", + update_interval=timedelta(seconds=30), + ) + session = async_get_clientsession(hass) + self.client = MeaterApi(session) + + async def _async_setup(self) -> None: + """Set up the Meater Coordinator.""" + try: + _LOGGER.debug("Authenticating with the Meater API") + await self.client.authenticate( + self.config_entry.data[CONF_USERNAME], + self.config_entry.data[CONF_PASSWORD], + ) + except (ServiceUnavailableError, TooManyRequestsError) as err: + raise UpdateFailed from err + except AuthenticationError as err: + raise ConfigEntryAuthFailed( + f"Unable to authenticate with the Meater API: {err}" + ) from err + + async def _async_update_data(self) -> dict[str, MeaterProbe]: + """Fetch data from API endpoint.""" + try: + # Note: TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with asyncio.timeout(10): + devices: list[MeaterProbe] = await self.client.get_all_devices() + except AuthenticationError as err: + raise ConfigEntryAuthFailed("The API call wasn't authenticated") from err + except TooManyRequestsError as err: + raise UpdateFailed( + "Too many requests have been made to the API, rate limiting is in place" + ) from err + + return {device.id: device for device in devices} diff --git a/homeassistant/components/meater/sensor.py b/homeassistant/components/meater/sensor.py index 00fc28b8718..cf1c72de85e 100644 --- a/homeassistant/components/meater/sensor.py +++ b/homeassistant/components/meater/sensor.py @@ -19,12 +19,10 @@ from homeassistant.const import UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, -) +from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.util import dt as dt_util +from . import MeaterCoordinator from .const import DOMAIN @@ -141,9 +139,7 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the entry.""" - coordinator: DataUpdateCoordinator[dict[str, MeaterProbe]] = hass.data[DOMAIN][ - entry.entry_id - ]["coordinator"] + coordinator: MeaterCoordinator = hass.data[DOMAIN][entry.entry_id] @callback def async_update_data(): @@ -176,9 +172,7 @@ async def async_setup_entry( coordinator.async_add_listener(async_update_data) -class MeaterProbeTemperature( - SensorEntity, CoordinatorEntity[DataUpdateCoordinator[dict[str, MeaterProbe]]] -): +class MeaterProbeTemperature(SensorEntity, CoordinatorEntity[MeaterCoordinator]): """Meater Temperature Sensor Entity.""" entity_description: MeaterSensorEntityDescription diff --git a/tests/components/meater/test_config_flow.py b/tests/components/meater/test_config_flow.py index 9049cf4ac9a..bb4055c7fae 100644 --- a/tests/components/meater/test_config_flow.py +++ b/tests/components/meater/test_config_flow.py @@ -23,7 +23,9 @@ def mock_client(): @pytest.fixture def mock_meater(mock_client): """Mock the meater library.""" - with patch("homeassistant.components.meater.MeaterApi.authenticate") as mock_: + with patch( + "homeassistant.components.meater.coordinator.MeaterApi.authenticate" + ) as mock_: mock_.side_effect = mock_client yield mock_