Move met coordinator to own module (#103546)

This commit is contained in:
Jan-Philipp Benecke 2023-11-08 16:55:09 +01:00 committed by GitHub
parent 241e8560e9
commit 5901f6f7e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 120 deletions

View File

@ -1,29 +1,12 @@
"""The met component.""" """The met component."""
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable
from datetime import timedelta
import logging 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.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import Platform
CONF_ELEVATION, from homeassistant.core import HomeAssistant
CONF_LATITUDE,
CONF_LONGITUDE,
EVENT_CORE_CONFIG_UPDATE,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr 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 ( from .const import (
CONF_TRACK_HOME, CONF_TRACK_HOME,
@ -31,9 +14,7 @@ from .const import (
DEFAULT_HOME_LONGITUDE, DEFAULT_HOME_LONGITUDE,
DOMAIN, DOMAIN,
) )
from .coordinator import MetDataUpdateCoordinator
# Dedicated Home Assistant endpoint - do not change!
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/complete"
PLATFORMS = [Platform.WEATHER] PLATFORMS = [Platform.WEATHER]
@ -98,98 +79,3 @@ async def cleanup_old_device(hass: HomeAssistant) -> None:
if device: if device:
_LOGGER.debug("Removing improper device %s", device.name) _LOGGER.debug("Removing improper device %s", device.name)
device_reg.async_remove_device(device.id) 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

View File

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

View File

@ -36,7 +36,6 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import METRIC_SYSTEM from homeassistant.util.unit_system import METRIC_SYSTEM
from . import MetDataUpdateCoordinator
from .const import ( from .const import (
ATTR_CONDITION_CLEAR_NIGHT, ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY, ATTR_CONDITION_SUNNY,
@ -46,6 +45,7 @@ from .const import (
DOMAIN, DOMAIN,
FORECAST_MAP, FORECAST_MAP,
) )
from .coordinator import MetDataUpdateCoordinator
DEFAULT_NAME = "Met.no" DEFAULT_NAME = "Met.no"

View File

@ -21,7 +21,7 @@ async def init_integration(hass, track_home=False) -> MockConfigEntry:
entry = MockConfigEntry(domain=DOMAIN, data=entry_data) entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
with patch( with patch(
"homeassistant.components.met.metno.MetWeatherData.fetching_data", "homeassistant.components.met.coordinator.metno.MetWeatherData.fetching_data",
return_value=True, return_value=True,
): ):
entry.add_to_hass(hass) entry.add_to_hass(hass)

View File

@ -156,7 +156,9 @@ async def test_options_flow(hass: HomeAssistant) -> None:
assert result["step_id"] == "init" assert result["step_id"] == "init"
# Test Options flow updated config entry # 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( result = await hass.config_entries.options.async_init(
entry.entry_id, data=update_data entry.entry_id, data=update_data
) )