Skip to content

Commit

Permalink
Add DataUpdateCoordinator to met integration (home-assistant#38405)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
bruxy70 and ctalkington authored Aug 9, 2020
1 parent 7610675 commit b258e75
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 95 deletions.
133 changes: 131 additions & 2 deletions homeassistant/components/met/__init__.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,153 @@
"""The met component."""
from datetime import timedelta
import logging
from random import randrange

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/"

from .config_flow import MetFlowHandler # noqa: F401
from .const import DOMAIN # noqa: F401

_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
8 changes: 8 additions & 0 deletions homeassistant/components/met/config_flow.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -71,6 +73,12 @@ async def _show_config_form(
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(
Expand Down
118 changes: 25 additions & 93 deletions homeassistant/components/met/weather.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,26 @@
"""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,
PRESSURE_INHG,
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__)

Expand All @@ -36,7 +30,6 @@
)
DEFAULT_NAME = "Met.no"

URL = "https://aa015h6buqvih86i1.api.met.no/weatherapi/locationforecast/2.0/classic"

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
Expand All @@ -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):
Expand Down Expand Up @@ -191,12 +123,12 @@ def name(self):
@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):
Expand All @@ -206,7 +138,7 @@ def temperature_unit(self):
@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

Expand All @@ -215,12 +147,12 @@ def pressure(self):
@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

Expand All @@ -231,7 +163,7 @@ def wind_speed(self):
@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):
Expand All @@ -241,4 +173,4 @@ def attribution(self):
@property
def forecast(self):
"""Return the forecast array."""
return self._forecast_data
return self._coordinator.data.forecast_data
Loading

0 comments on commit b258e75

Please sign in to comment.