mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 01:08:12 +00:00
Set up Met during onboarding (#24622)
* Set up Met during onboarding * Lint * Add pyMetNo to test reqs
This commit is contained in:
parent
6ea92f86a5
commit
f2962a0d16
@ -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
|
||||
}
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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({})
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -90,6 +90,7 @@ TEST_REQUIREMENTS = (
|
||||
'libpurecool',
|
||||
'libsoundtouch',
|
||||
'luftdaten',
|
||||
'pyMetno',
|
||||
'mbddns',
|
||||
'mficlient',
|
||||
'netdisco',
|
||||
|
1
tests/components/met/__init__.py
Normal file
1
tests/components/met/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for Met.no."""
|
24
tests/components/met/conftest.py
Normal file
24
tests/components/met/conftest.py
Normal 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
|
@ -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,
|
||||
}
|
||||
|
52
tests/components/met/test_weather.py
Normal file
52
tests/components/met/test_weather.py
Normal 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
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user