diff --git a/homeassistant/components/met/__init__.py b/homeassistant/components/met/__init__.py index 53764252043..a10a07b5374 100644 --- a/homeassistant/components/met/__init__.py +++ b/homeassistant/components/met/__init__.py @@ -1,29 +1,12 @@ """The met component.""" from __future__ import annotations -from collections.abc import Callable -from datetime import timedelta import logging -from random import randrange -from types import MappingProxyType -from typing import Any, Self - -import metno from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_ELEVATION, - CONF_LATITUDE, - CONF_LONGITUDE, - EVENT_CORE_CONFIG_UPDATE, - Platform, -) -from homeassistant.core import Event, HomeAssistant -from homeassistant.exceptions import HomeAssistantError +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from homeassistant.util import dt as dt_util from .const import ( CONF_TRACK_HOME, @@ -31,9 +14,7 @@ from .const import ( DEFAULT_HOME_LONGITUDE, DOMAIN, ) - -# Dedicated Home Assistant endpoint - do not change! -URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" +from .coordinator import MetDataUpdateCoordinator PLATFORMS = [Platform.WEATHER] @@ -98,98 +79,3 @@ async def cleanup_old_device(hass: HomeAssistant) -> None: if device: _LOGGER.debug("Removing improper device %s", device.name) device_reg.async_remove_device(device.id) - - -class CannotConnect(HomeAssistantError): - """Unable to connect to the web site.""" - - -class MetDataUpdateCoordinator(DataUpdateCoordinator["MetWeatherData"]): - """Class to manage fetching Met data.""" - - def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: - """Initialize global Met data updater.""" - self._unsub_track_home: Callable[[], None] | None = None - self.weather = MetWeatherData(hass, config_entry.data) - self.weather.set_coordinates() - - update_interval = timedelta(minutes=randrange(55, 65)) - - super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - - async def _async_update_data(self) -> MetWeatherData: - """Fetch data from Met.""" - try: - return await self.weather.fetch_data() - except Exception as err: - raise UpdateFailed(f"Update failed: {err}") from err - - def track_home(self) -> None: - """Start tracking changes to HA home setting.""" - if self._unsub_track_home: - return - - async def _async_update_weather_data(_event: Event | None = None) -> None: - """Update weather data.""" - if self.weather.set_coordinates(): - await self.async_refresh() - - self._unsub_track_home = self.hass.bus.async_listen( - EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data - ) - - def untrack_home(self) -> None: - """Stop tracking changes to HA home setting.""" - if self._unsub_track_home: - self._unsub_track_home() - self._unsub_track_home = None - - -class MetWeatherData: - """Keep data for Met.no weather entities.""" - - def __init__(self, hass: HomeAssistant, config: MappingProxyType[str, Any]) -> None: - """Initialise the weather entity data.""" - self.hass = hass - self._config = config - self._weather_data: metno.MetWeatherData - self.current_weather_data: dict = {} - self.daily_forecast: list[dict] = [] - self.hourly_forecast: list[dict] = [] - self._coordinates: dict[str, str] | None = None - - def set_coordinates(self) -> bool: - """Weather data initialization - set the coordinates.""" - if self._config.get(CONF_TRACK_HOME, False): - latitude = self.hass.config.latitude - longitude = self.hass.config.longitude - elevation = self.hass.config.elevation - else: - latitude = self._config[CONF_LATITUDE] - longitude = self._config[CONF_LONGITUDE] - elevation = self._config[CONF_ELEVATION] - - coordinates = { - "lat": str(latitude), - "lon": str(longitude), - "msl": str(elevation), - } - if coordinates == self._coordinates: - return False - self._coordinates = coordinates - - self._weather_data = metno.MetWeatherData( - coordinates, async_get_clientsession(self.hass), api_url=URL - ) - return True - - async def fetch_data(self) -> Self: - """Fetch data from API - (current weather and forecast).""" - resp = await self._weather_data.fetching_data() - if not resp: - raise CannotConnect() - self.current_weather_data = self._weather_data.get_current_weather() - time_zone = dt_util.DEFAULT_TIME_ZONE - self.daily_forecast = self._weather_data.get_forecast(time_zone, False, 0) - self.hourly_forecast = self._weather_data.get_forecast(time_zone, True) - return self diff --git a/homeassistant/components/met/coordinator.py b/homeassistant/components/met/coordinator.py new file mode 100644 index 00000000000..6354e286cee --- /dev/null +++ b/homeassistant/components/met/coordinator.py @@ -0,0 +1,126 @@ +"""DataUpdateCoordinator for Met.no integration.""" +from __future__ import annotations + +from collections.abc import Callable +from datetime import timedelta +import logging +from random import randrange +from types import MappingProxyType +from typing import Any, Self + +import metno + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ELEVATION, + CONF_LATITUDE, + CONF_LONGITUDE, + EVENT_CORE_CONFIG_UPDATE, +) +from homeassistant.core import Event, HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util import dt as dt_util + +from .const import CONF_TRACK_HOME, DOMAIN + +# Dedicated Home Assistant endpoint - do not change! +URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete" + +_LOGGER = logging.getLogger(__name__) + + +class CannotConnect(HomeAssistantError): + """Unable to connect to the web site.""" + + +class MetWeatherData: + """Keep data for Met.no weather entities.""" + + def __init__(self, hass: HomeAssistant, config: MappingProxyType[str, Any]) -> None: + """Initialise the weather entity data.""" + self.hass = hass + self._config = config + self._weather_data: metno.MetWeatherData + self.current_weather_data: dict = {} + self.daily_forecast: list[dict] = [] + self.hourly_forecast: list[dict] = [] + self._coordinates: dict[str, str] | None = None + + def set_coordinates(self) -> bool: + """Weather data initialization - set the coordinates.""" + if self._config.get(CONF_TRACK_HOME, False): + latitude = self.hass.config.latitude + longitude = self.hass.config.longitude + elevation = self.hass.config.elevation + else: + latitude = self._config[CONF_LATITUDE] + longitude = self._config[CONF_LONGITUDE] + elevation = self._config[CONF_ELEVATION] + + coordinates = { + "lat": str(latitude), + "lon": str(longitude), + "msl": str(elevation), + } + if coordinates == self._coordinates: + return False + self._coordinates = coordinates + + self._weather_data = metno.MetWeatherData( + coordinates, async_get_clientsession(self.hass), api_url=URL + ) + return True + + async def fetch_data(self) -> Self: + """Fetch data from API - (current weather and forecast).""" + resp = await self._weather_data.fetching_data() + if not resp: + raise CannotConnect() + self.current_weather_data = self._weather_data.get_current_weather() + time_zone = dt_util.DEFAULT_TIME_ZONE + self.daily_forecast = self._weather_data.get_forecast(time_zone, False, 0) + self.hourly_forecast = self._weather_data.get_forecast(time_zone, True) + return self + + +class MetDataUpdateCoordinator(DataUpdateCoordinator[MetWeatherData]): + """Class to manage fetching Met data.""" + + def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None: + """Initialize global Met data updater.""" + self._unsub_track_home: Callable[[], None] | None = None + self.weather = MetWeatherData(hass, config_entry.data) + self.weather.set_coordinates() + + update_interval = timedelta(minutes=randrange(55, 65)) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self) -> MetWeatherData: + """Fetch data from Met.""" + try: + return await self.weather.fetch_data() + except Exception as err: + raise UpdateFailed(f"Update failed: {err}") from err + + def track_home(self) -> None: + """Start tracking changes to HA home setting.""" + if self._unsub_track_home: + return + + async def _async_update_weather_data(_event: Event | None = None) -> None: + """Update weather data.""" + if self.weather.set_coordinates(): + await self.async_refresh() + + self._unsub_track_home = self.hass.bus.async_listen( + EVENT_CORE_CONFIG_UPDATE, _async_update_weather_data + ) + + def untrack_home(self) -> None: + """Stop tracking changes to HA home setting.""" + if self._unsub_track_home: + self._unsub_track_home() + self._unsub_track_home = None diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 97b99e826cd..11b044311d2 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -36,7 +36,6 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.unit_system import METRIC_SYSTEM -from . import MetDataUpdateCoordinator from .const import ( ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_SUNNY, @@ -46,6 +45,7 @@ from .const import ( DOMAIN, FORECAST_MAP, ) +from .coordinator import MetDataUpdateCoordinator DEFAULT_NAME = "Met.no" diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index 0a17b415965..2ef0f7e12f0 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -21,7 +21,7 @@ async def init_integration(hass, track_home=False) -> MockConfigEntry: entry = MockConfigEntry(domain=DOMAIN, data=entry_data) with patch( - "homeassistant.components.met.metno.MetWeatherData.fetching_data", + "homeassistant.components.met.coordinator.metno.MetWeatherData.fetching_data", return_value=True, ): entry.add_to_hass(hass) diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 59ffff14f1b..24ce8660346 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -156,7 +156,9 @@ async def test_options_flow(hass: HomeAssistant) -> None: assert result["step_id"] == "init" # Test Options flow updated config entry - with patch("homeassistant.components.met.metno.MetWeatherData") as weatherdatamock: + with patch( + "homeassistant.components.met.coordinator.metno.MetWeatherData" + ) as weatherdatamock: result = await hass.config_entries.options.async_init( entry.entry_id, data=update_data )