Set up Met during onboarding (#24622)

* Set up Met during onboarding

* Lint

* Add pyMetNo to test reqs
This commit is contained in:
Paulus Schoutsen 2019-06-19 14:41:27 -07:00 committed by GitHub
parent 6ea92f86a5
commit f2962a0d16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 242 additions and 54 deletions

View File

@ -7,7 +7,7 @@ from homeassistant.const import (
from homeassistant.core import callback
import homeassistant.helpers.config_validation as cv
from .const import DOMAIN, HOME_LOCATION_NAME
from .const import DOMAIN, HOME_LOCATION_NAME, CONF_TRACK_HOME
@callback
@ -61,3 +61,12 @@ class MetFlowHandler(data_entry_flow.FlowHandler):
}),
errors=self._errors,
)
async def async_step_onboarding(self, data=None):
"""Handle a flow initialized by onboarding."""
return self.async_create_entry(
title=HOME_LOCATION_NAME,
data={
CONF_TRACK_HOME: True
}
)

View File

@ -7,6 +7,8 @@ DOMAIN = 'met'
HOME_LOCATION_NAME = 'Home'
CONF_TRACK_HOME = 'track_home'
ENTITY_ID_SENSOR_FORMAT = WEATHER_DOMAIN + ".met_{}"
ENTITY_ID_SENSOR_FORMAT_HOME = ENTITY_ID_SENSOR_FORMAT.format(
HOME_LOCATION_NAME)

View File

@ -2,17 +2,21 @@
import logging
from random import randrange
import metno
import voluptuous as vol
from homeassistant.core import callback
from homeassistant.components.weather import PLATFORM_SCHEMA, WeatherEntity
from homeassistant.const import (
CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS)
CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS,
EVENT_CORE_CONFIG_UPDATE)
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, async_track_utc_time_change)
from homeassistant.helpers.event import async_call_later
import homeassistant.util.dt as dt_util
from .const import CONF_TRACK_HOME
_LOGGER = logging.getLogger(__name__)
ATTRIBUTION = "Weather forecast from met.no, delivered by the Norwegian " \
@ -27,6 +31,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
'Latitude and longitude must exist together'): cv.latitude,
vol.Inclusive(CONF_LONGITUDE, 'coordinates',
'Latitude and longitude must exist together'): cv.longitude,
vol.Optional(CONF_ELEVATION): int,
})
@ -35,60 +40,82 @@ async def async_setup_platform(hass, config, async_add_entities,
"""Set up the Met.no weather platform."""
_LOGGER.warning("Loading Met.no via platform config is deprecated")
name = config.get(CONF_NAME)
latitude = config.get(CONF_LATITUDE, hass.config.latitude)
longitude = config.get(CONF_LONGITUDE, hass.config.longitude)
elevation = config.get(CONF_ELEVATION, hass.config.elevation or 0)
# Add defaults.
config = {
CONF_ELEVATION: hass.config.elevation,
**config,
}
if None in (latitude, longitude):
_LOGGER.error("Latitude or longitude not set in Home Assistant config")
return
if config.get(CONF_LATITUDE) is None:
config[CONF_TRACK_HOME] = True
station = await async_get_station(
hass, name, latitude, longitude, elevation)
async_add_entities([station])
async_add_entities([MetWeather(config)])
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Add a weather entity from a config_entry."""
name = config_entry.data.get(CONF_NAME)
latitude = config_entry.data[CONF_LATITUDE]
longitude = config_entry.data[CONF_LONGITUDE]
elevation = config_entry.data[CONF_ELEVATION]
station = await async_get_station(
hass, name, latitude, longitude, elevation)
async_add_entities([station])
async def async_get_station(hass, name, latitude, longitude, elevation):
"""Retrieve weather station, station name to be used as the entity name."""
coordinates = {
'lat': str(latitude),
'lon': str(longitude),
'msl': str(elevation),
}
return MetWeather(name, coordinates, async_get_clientsession(hass))
async_add_entities([MetWeather(config_entry.data)])
class MetWeather(WeatherEntity):
"""Implementation of a Met.no weather condition."""
def __init__(self, name, coordinates, clientsession):
def __init__(self, config):
"""Initialise the platform with a data instance and site."""
import metno
self._name = name
self._weather_data = metno.MetWeatherData(
coordinates, clientsession, URL)
self._config = config
self._unsub_track_home = None
self._unsub_fetch_data = None
self._weather_data = None
self._current_weather_data = {}
self._forecast_data = None
async def async_added_to_hass(self):
"""Start fetching data."""
self._init_data()
await self._fetch_data()
async_track_utc_time_change(
self.hass, self._update, minute=31, second=0)
if self._config.get(CONF_TRACK_HOME):
self._unsub_track_home = self.hass.bus.async_listen(
EVENT_CORE_CONFIG_UPDATE, self._core_config_updated)
@callback
def _init_data(self):
"""Initialize a data object."""
conf = self._config
if self.track_home:
latitude = self.hass.config.latitude
longitude = self.hass.config.longitude
elevation = self.hass.config.elevation
else:
latitude = conf[CONF_LATITUDE]
longitude = conf[CONF_LONGITUDE]
elevation = conf[CONF_ELEVATION]
coordinates = {
'lat': str(latitude),
'lon': str(longitude),
'msl': str(elevation),
}
self._weather_data = metno.MetWeatherData(
coordinates, async_get_clientsession(self.hass), URL)
async def _core_config_updated(self, _event):
"""Handle core config updated."""
self._init_data()
if self._unsub_fetch_data:
self._unsub_fetch_data()
self._unsub_fetch_data = None
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."""
@ -96,28 +123,55 @@ class MetWeather(WeatherEntity):
# Retry in 15 to 20 minutes.
minutes = 15 + randrange(6)
_LOGGER.error("Retrying in %i minutes", minutes)
async_call_later(self.hass, minutes*60, self._fetch_data)
self._unsub_fetch_data = async_call_later(
self.hass, minutes*60, self._fetch_data)
return
async_call_later(self.hass, 60*60, self._fetch_data)
await self._update()
# 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._update()
def _update(self, *_):
"""Get the latest data from Met.no."""
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()
@property
def track_home(self):
"""Return if we are tracking home."""
return self._config.get(CONF_TRACK_HOME, False)
@property
def should_poll(self):
"""No polling needed."""
return False
async def _update(self, *_):
"""Get the latest data from Met.no."""
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_schedule_update_ha_state()
@property
def unique_id(self):
"""Return unique ID."""
if self.track_home:
return 'home'
return '{}-{}'.format(
self._config[CONF_LATITUDE], self._config[CONF_LONGITUDE])
@property
def name(self):
"""Return the name of the sensor."""
return self._name
name = self._config.get(CONF_NAME)
if name is not None:
return CONF_NAME
if self.track_home:
return self.hass.config.location_name
return DEFAULT_NAME
@property
def condition(self):

View File

@ -148,6 +148,10 @@ class CoreConfigOnboardingView(_BaseOnboardingView):
await self._async_mark_done(hass)
await hass.config_entries.flow.async_init('met', context={
'source': 'onboarding'
})
return self.json({})

View File

@ -60,10 +60,6 @@ default_config:
# http:
# base_url: example.duckdns.org:8123
# Weather prediction
weather:
- platform: met
# Text to speech
tts:
- platform: google_translate

View File

@ -238,6 +238,10 @@ py-canary==0.5.0
# homeassistant.components.tplink
pyHS100==0.3.5
# homeassistant.components.met
# homeassistant.components.norway_air
pyMetno==0.4.6
# homeassistant.components.blackbird
pyblackbird==0.5

View File

@ -90,6 +90,7 @@ TEST_REQUIREMENTS = (
'libpurecool',
'libsoundtouch',
'luftdaten',
'pyMetno',
'mbddns',
'mficlient',
'netdisco',

View File

@ -0,0 +1 @@
"""Tests for Met.no."""

View File

@ -0,0 +1,24 @@
"""Fixtures for Met weather testing."""
from unittest.mock import patch
import pytest
from tests.common import mock_coro
@pytest.fixture
def mock_weather():
"""Mock weather data."""
with patch('metno.MetWeatherData') as mock_data:
mock_data = mock_data.return_value
mock_data.fetching_data.side_effect = lambda: mock_coro(True)
mock_data.get_current_weather.return_value = {
'condition': 'cloudy',
'temperature': 15,
'pressure': 100,
'humidity': 50,
'wind_speed': 10,
'wind_bearing': 'NE',
}
mock_data.get_forecast.return_value = {}
yield mock_data

View File

@ -131,3 +131,19 @@ async def test_flow_entry_config_entry_already_exists():
assert len(config_form.mock_calls) == 1
assert len(config_entries.mock_calls) == 1
assert len(flow._errors) == 1
async def test_onboarding_step(hass, mock_weather):
"""Test initializing via onboarding step."""
hass = Mock()
flow = config_flow.MetFlowHandler()
flow.hass = hass
result = await flow.async_step_onboarding({})
assert result['type'] == 'create_entry'
assert result['title'] == 'Home'
assert result['data'] == {
'track_home': True,
}

View File

@ -0,0 +1,52 @@
"""Test Met weather entity."""
async def test_tracking_home(hass, mock_weather):
"""Test we track home."""
await hass.config_entries.flow.async_init('met', context={
'source': 'onboarding'
})
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids('weather')) == 1
assert len(mock_weather.mock_calls) == 3
# Test we track config
await hass.config.async_update(
latitude=10,
longitude=20,
)
await hass.async_block_till_done()
assert len(mock_weather.mock_calls) == 6
entry = hass.config_entries.async_entries()[0]
await hass.config_entries.async_remove(entry.entry_id)
assert len(hass.states.async_entity_ids('weather')) == 0
async def test_not_tracking_home(hass, mock_weather):
"""Test when we not track home."""
await hass.config_entries.flow.async_init('met', context={
'source': 'user'
}, data={
'name': 'Somewhere',
'latitude': 10,
'longitude': 20,
'elevation': 0,
})
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids('weather')) == 1
assert len(mock_weather.mock_calls) == 3
# Test we do not track config
await hass.config.async_update(
latitude=10,
longitude=20,
)
await hass.async_block_till_done()
assert len(mock_weather.mock_calls) == 3
entry = hass.config_entries.async_entries()[0]
await hass.config_entries.async_remove(entry.entry_id)
assert len(hass.states.async_entity_ids('weather')) == 0

View File

@ -9,10 +9,17 @@ from homeassistant.components import onboarding
from homeassistant.components.onboarding import const, views
from tests.common import CLIENT_ID, register_auth_provider
from tests.components.met.conftest import mock_weather # noqa
from . import mock_storage
@pytest.fixture(autouse=True)
def always_mock_weather(mock_weather): # noqa
"""Mock the Met weather provider."""
pass
@pytest.fixture(autouse=True)
def auth_active(hass):
"""Ensure auth is always active."""
@ -224,3 +231,21 @@ async def test_onboarding_integration_requires_auth(hass, hass_storage,
})
assert resp.status == 401
async def test_onboarding_core_sets_up_met(hass, hass_storage, hass_client):
"""Test finishing the core step."""
mock_storage(hass_storage, {
'done': [const.STEP_USER]
})
assert await async_setup_component(hass, 'onboarding', {})
client = await hass_client()
resp = await client.post('/api/onboarding/core_config')
assert resp.status == 200
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids('weather')) == 1