mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 09:47:52 +00:00
Add DataUpdateCoordinator to met integration (#38405)
* Add DataUpdateCoordinator to met integration * isort * redundant fetch_data * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * Update homeassistant/components/met/weather.py Co-authored-by: Chris Talkington <chris@talkingtontech.com> * fix black, isort, flake8, hassfest, mypy * remove unused async_setup method * replace fetch_data by coordinator request_refresh * remove redundant async_update * track_home * Apply suggestions from code review * Apply suggestions from code review * Update homeassistant/components/met/__init__.py * Apply suggestions from code review * Update homeassistant/components/met/__init__.py * Apply suggestions from code review * Update homeassistant/components/met/__init__.py * Apply suggestions from code review * Update test_config_flow.py * Apply suggestions from code review * Apply suggestions from code review * Update __init__.py * Create test_init.py * Update homeassistant/components/met/__init__.py * Update __init__.py * Update __init__.py * Update homeassistant/components/met/__init__.py Co-authored-by: Chris Talkington <chris@talkingtontech.com>
This commit is contained in:
parent
761067559d
commit
b258e757c2
@ -1,24 +1,153 @@
|
||||
"""The met component."""
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from random import randrange
|
||||
|
||||
from .config_flow import MetFlowHandler # noqa: F401
|
||||
from .const import DOMAIN # noqa: F401
|
||||
import metno
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_ELEVATION,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
LENGTH_FEET,
|
||||
LENGTH_METERS,
|
||||
)
|
||||
from homeassistant.core import Config, HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
from homeassistant.util.distance import convert as convert_distance
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import CONF_TRACK_HOME, DOMAIN
|
||||
|
||||
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/1.9/"
|
||||
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
|
||||
"""Set up configured Met."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up Met as config entry."""
|
||||
coordinator = MetDataUpdateCoordinator(hass, config_entry)
|
||||
await coordinator.async_refresh()
|
||||
|
||||
if not coordinator.last_update_success:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if config_entry.data.get(CONF_TRACK_HOME, False):
|
||||
coordinator.track_home()
|
||||
|
||||
hass.data[DOMAIN][config_entry.entry_id] = coordinator
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(config_entry, "weather")
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, config_entry):
|
||||
"""Unload a config entry."""
|
||||
await hass.config_entries.async_forward_entry_unload(config_entry, "weather")
|
||||
hass.data[DOMAIN][config_entry.entry_id].untrack_home()
|
||||
hass.data[DOMAIN].pop(config_entry.entry_id)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class MetDataUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Class to manage fetching Met data."""
|
||||
|
||||
def __init__(self, hass, config_entry):
|
||||
"""Initialize global Met data updater."""
|
||||
self._unsub_track_home = None
|
||||
self.weather = MetWeatherData(
|
||||
hass, config_entry.data, hass.config.units.is_metric
|
||||
)
|
||||
self.weather.init_data()
|
||||
|
||||
update_interval = timedelta(minutes=randrange(55, 65))
|
||||
|
||||
super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval)
|
||||
|
||||
async def _async_update_data(self):
|
||||
"""Fetch data from Met."""
|
||||
try:
|
||||
return await self.weather.fetch_data()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Update failed: {err}")
|
||||
|
||||
def track_home(self):
|
||||
"""Start tracking changes to HA home setting."""
|
||||
if self._unsub_track_home:
|
||||
return
|
||||
|
||||
async def _async_update_weather_data(_event=None):
|
||||
"""Update weather data."""
|
||||
self.weather.init_data()
|
||||
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):
|
||||
"""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, config, is_metric):
|
||||
"""Initialise the weather entity data."""
|
||||
self.hass = hass
|
||||
self._config = config
|
||||
self._is_metric = is_metric
|
||||
self._weather_data = None
|
||||
self.current_weather_data = {}
|
||||
self.forecast_data = None
|
||||
|
||||
def init_data(self):
|
||||
"""Weather data inialization - get 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]
|
||||
|
||||
if not self._is_metric:
|
||||
elevation = int(
|
||||
round(convert_distance(elevation, LENGTH_FEET, LENGTH_METERS))
|
||||
)
|
||||
|
||||
coordinates = {
|
||||
"lat": str(latitude),
|
||||
"lon": str(longitude),
|
||||
"msl": str(elevation),
|
||||
}
|
||||
|
||||
self._weather_data = metno.MetWeatherData(
|
||||
coordinates, async_get_clientsession(self.hass), URL
|
||||
)
|
||||
|
||||
async def fetch_data(self):
|
||||
"""Fetch data from API - (current weather and forecast)."""
|
||||
await self._weather_data.fetching_data()
|
||||
self.current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.DEFAULT_TIME_ZONE
|
||||
self.forecast_data = self._weather_data.get_forecast(time_zone)
|
||||
return self
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""Config flow to configure Met component."""
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
@ -71,6 +73,12 @@ class MetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
errors=self._errors,
|
||||
)
|
||||
|
||||
async def async_step_import(
|
||||
self, user_input: Optional[Dict] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Handle configuration by yaml file."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
async def async_step_onboarding(self, data=None):
|
||||
"""Handle a flow initialized by onboarding."""
|
||||
return self.async_create_entry(
|
||||
|
@ -1,18 +1,15 @@
|
||||
"""Support for Met.no weather service."""
|
||||
import logging
|
||||
from random import randrange
|
||||
|
||||
import metno
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
CONF_ELEVATION,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
CONF_NAME,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
LENGTH_FEET,
|
||||
LENGTH_METERS,
|
||||
LENGTH_MILES,
|
||||
PRESSURE_HPA,
|
||||
@ -20,13 +17,10 @@ from homeassistant.const import (
|
||||
TEMP_CELSIUS,
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.util.distance import convert as convert_distance
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.util.pressure import convert as convert_pressure
|
||||
|
||||
from .const import CONF_TRACK_HOME
|
||||
from .const import CONF_TRACK_HOME, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -36,7 +30,6 @@ ATTRIBUTION = (
|
||||
)
|
||||
DEFAULT_NAME = "Met.no"
|
||||
|
||||
URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/classic"
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
@ -62,100 +55,39 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
|
||||
if config.get(CONF_LATITUDE) is None:
|
||||
config[CONF_TRACK_HOME] = True
|
||||
|
||||
async_add_entities([MetWeather(config, hass.config.units.is_metric)])
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}, data=config
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||
"""Add a weather entity from a config_entry."""
|
||||
async_add_entities([MetWeather(config_entry.data, hass.config.units.is_metric)])
|
||||
coordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
async_add_entities(
|
||||
[MetWeather(coordinator, config_entry.data, hass.config.units.is_metric)]
|
||||
)
|
||||
|
||||
|
||||
class MetWeather(WeatherEntity):
|
||||
"""Implementation of a Met.no weather condition."""
|
||||
|
||||
def __init__(self, config, is_metric):
|
||||
def __init__(self, coordinator, config, is_metric):
|
||||
"""Initialise the platform with a data instance and site."""
|
||||
self._config = config
|
||||
self._coordinator = coordinator
|
||||
self._is_metric = is_metric
|
||||
self._unsub_track_home = None
|
||||
self._unsub_fetch_data = None
|
||||
self._weather_data = None
|
||||
self._current_weather_data = {}
|
||||
self._coordinates = {}
|
||||
self._forecast_data = None
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Start fetching data."""
|
||||
await self._init_data()
|
||||
if self._config.get(CONF_TRACK_HOME):
|
||||
self._unsub_track_home = self.hass.bus.async_listen(
|
||||
EVENT_CORE_CONFIG_UPDATE, self._init_data
|
||||
)
|
||||
|
||||
async def _init_data(self, _event=None):
|
||||
"""Initialize and fetch data object."""
|
||||
if self.track_home:
|
||||
latitude = self.hass.config.latitude
|
||||
longitude = self.hass.config.longitude
|
||||
elevation = self.hass.config.elevation
|
||||
else:
|
||||
conf = self._config
|
||||
latitude = conf[CONF_LATITUDE]
|
||||
longitude = conf[CONF_LONGITUDE]
|
||||
elevation = conf[CONF_ELEVATION]
|
||||
|
||||
if not self._is_metric:
|
||||
elevation = convert_distance(elevation, LENGTH_FEET, LENGTH_METERS)
|
||||
coordinates = {
|
||||
"lat": latitude,
|
||||
"lon": longitude,
|
||||
"msl": elevation,
|
||||
}
|
||||
if coordinates == self._coordinates:
|
||||
return
|
||||
self._coordinates = coordinates
|
||||
|
||||
self._weather_data = metno.MetWeatherData(
|
||||
coordinates, async_get_clientsession(self.hass), URL
|
||||
)
|
||||
await self._fetch_data()
|
||||
|
||||
async def will_remove_from_hass(self):
|
||||
"""Handle entity will be removed from hass."""
|
||||
if self._unsub_track_home:
|
||||
self._unsub_track_home()
|
||||
self._unsub_track_home = None
|
||||
|
||||
if self._unsub_fetch_data:
|
||||
self._unsub_fetch_data()
|
||||
self._unsub_fetch_data = None
|
||||
|
||||
async def _fetch_data(self, *_):
|
||||
"""Get the latest data from met.no."""
|
||||
if self._unsub_fetch_data:
|
||||
self._unsub_fetch_data()
|
||||
self._unsub_fetch_data = None
|
||||
|
||||
if not await self._weather_data.fetching_data():
|
||||
# Retry in 15 to 20 minutes.
|
||||
minutes = 15 + randrange(6)
|
||||
_LOGGER.error("Retrying in %i minutes", minutes)
|
||||
self._unsub_fetch_data = async_call_later(
|
||||
self.hass, minutes * 60, self._fetch_data
|
||||
)
|
||||
return
|
||||
|
||||
# Wait between 55-65 minutes. If people update HA on the hour, this
|
||||
# will make sure it will spread it out.
|
||||
|
||||
self._unsub_fetch_data = async_call_later(
|
||||
self.hass, randrange(55, 65) * 60, self._fetch_data
|
||||
self.async_on_remove(
|
||||
self._coordinator.async_add_listener(self.async_write_ha_state)
|
||||
)
|
||||
|
||||
self._current_weather_data = self._weather_data.get_current_weather()
|
||||
time_zone = dt_util.DEFAULT_TIME_ZONE
|
||||
self._forecast_data = self._weather_data.get_forecast(time_zone)
|
||||
self.async_write_ha_state()
|
||||
async def async_update(self):
|
||||
"""Only used by the generic entity update service."""
|
||||
await self._coordinator.async_request_refresh()
|
||||
|
||||
@property
|
||||
def track_home(self):
|
||||
@ -191,12 +123,12 @@ class MetWeather(WeatherEntity):
|
||||
@property
|
||||
def condition(self):
|
||||
"""Return the current condition."""
|
||||
return self._current_weather_data.get("condition")
|
||||
return self._coordinator.data.current_weather_data.get("condition")
|
||||
|
||||
@property
|
||||
def temperature(self):
|
||||
"""Return the temperature."""
|
||||
return self._current_weather_data.get("temperature")
|
||||
return self._coordinator.data.current_weather_data.get("temperature")
|
||||
|
||||
@property
|
||||
def temperature_unit(self):
|
||||
@ -206,7 +138,7 @@ class MetWeather(WeatherEntity):
|
||||
@property
|
||||
def pressure(self):
|
||||
"""Return the pressure."""
|
||||
pressure_hpa = self._current_weather_data.get("pressure")
|
||||
pressure_hpa = self._coordinator.data.current_weather_data.get("pressure")
|
||||
if self._is_metric or pressure_hpa is None:
|
||||
return pressure_hpa
|
||||
|
||||
@ -215,12 +147,12 @@ class MetWeather(WeatherEntity):
|
||||
@property
|
||||
def humidity(self):
|
||||
"""Return the humidity."""
|
||||
return self._current_weather_data.get("humidity")
|
||||
return self._coordinator.data.current_weather_data.get("humidity")
|
||||
|
||||
@property
|
||||
def wind_speed(self):
|
||||
"""Return the wind speed."""
|
||||
speed_m_s = self._current_weather_data.get("wind_speed")
|
||||
speed_m_s = self._coordinator.data.current_weather_data.get("wind_speed")
|
||||
if self._is_metric or speed_m_s is None:
|
||||
return speed_m_s
|
||||
|
||||
@ -231,7 +163,7 @@ class MetWeather(WeatherEntity):
|
||||
@property
|
||||
def wind_bearing(self):
|
||||
"""Return the wind direction."""
|
||||
return self._current_weather_data.get("wind_bearing")
|
||||
return self._coordinator.data.current_weather_data.get("wind_bearing")
|
||||
|
||||
@property
|
||||
def attribution(self):
|
||||
@ -241,4 +173,4 @@ class MetWeather(WeatherEntity):
|
||||
@property
|
||||
def forecast(self):
|
||||
"""Return the forecast array."""
|
||||
return self._forecast_data
|
||||
return self._coordinator.data.forecast_data
|
||||
|
@ -1 +1,26 @@
|
||||
"""Tests for Met.no."""
|
||||
from homeassistant.components.met.const import DOMAIN
|
||||
from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME
|
||||
|
||||
from tests.async_mock import patch
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def init_integration(hass) -> MockConfigEntry:
|
||||
"""Set up the Met integration in Home Assistant."""
|
||||
entry_data = {
|
||||
CONF_NAME: "test",
|
||||
CONF_LATITUDE: 0,
|
||||
CONF_LONGITUDE: 0,
|
||||
CONF_ELEVATION: 0,
|
||||
}
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=entry_data)
|
||||
with patch(
|
||||
"homeassistant.components.met.metno.MetWeatherData.fetching_data",
|
||||
return_value=True,
|
||||
):
|
||||
entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
|
@ -103,3 +103,21 @@ async def test_onboarding_step(hass):
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == HOME_LOCATION_NAME
|
||||
assert result["data"] == {"track_home": True}
|
||||
|
||||
|
||||
async def test_import_step(hass):
|
||||
"""Test initializing via import step."""
|
||||
test_data = {
|
||||
"name": "home",
|
||||
CONF_LONGITUDE: None,
|
||||
CONF_LATITUDE: None,
|
||||
CONF_ELEVATION: 0,
|
||||
"track_home": True,
|
||||
}
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": "import"}, data=test_data
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
assert result["title"] == "home"
|
||||
assert result["data"] == test_data
|
||||
|
19
tests/components/met/test_init.py
Normal file
19
tests/components/met/test_init.py
Normal file
@ -0,0 +1,19 @@
|
||||
"""Test the Met integration init."""
|
||||
from homeassistant.components.met.const import DOMAIN
|
||||
from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED
|
||||
|
||||
from . import init_integration
|
||||
|
||||
|
||||
async def test_unload_entry(hass):
|
||||
"""Test successful unload of entry."""
|
||||
entry = await init_integration(hass)
|
||||
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state == ENTRY_STATE_LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ENTRY_STATE_NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
Loading…
x
Reference in New Issue
Block a user