Swedish weather institute weather component (#16717)
* SMHI Component * Clean up typos * Fixed default values first config to home location (tests will follow) * Fixed tests and removed unused function * Minor fixup after comments from @kane610 * add support for precipitation in forecast * Removed old async_step_init not needed.
This commit is contained in:
parent
56a43436d7
commit
540d22d603
18 changed files with 2716 additions and 2 deletions
19
homeassistant/components/smhi/.translations/en.json
Normal file
19
homeassistant/components/smhi/.translations/en.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"name_exists": "Name already exists",
|
||||||
|
"wrong_location": "Location in Sweden only"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude",
|
||||||
|
"name": "Name"
|
||||||
|
},
|
||||||
|
"title": "Location in Sweden"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "Swedish weather service (SMHI)"
|
||||||
|
}
|
||||||
|
}
|
19
homeassistant/components/smhi/.translations/sv.json
Normal file
19
homeassistant/components/smhi/.translations/sv.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"error": {
|
||||||
|
"name_exists": "Namnet finns redan",
|
||||||
|
"wrong_location": "Endast plats i Sverige"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"name": "Namn"
|
||||||
|
},
|
||||||
|
"title": "Plats i Sverige"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"title": "SMHI svenskt väder"
|
||||||
|
}
|
||||||
|
}
|
39
homeassistant/components/smhi/__init__.py
Normal file
39
homeassistant/components/smhi/__init__.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
"""
|
||||||
|
Component for the swedish weather institute weather service.
|
||||||
|
|
||||||
|
For more details about this component, please refer to the documentation at
|
||||||
|
https://home-assistant.io/components/smhi/
|
||||||
|
"""
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import Config, HomeAssistant
|
||||||
|
|
||||||
|
# Have to import for config_flow to work
|
||||||
|
# even if they are not used here
|
||||||
|
from .config_flow import smhi_locations # noqa: F401
|
||||||
|
from .const import DOMAIN # noqa: F401
|
||||||
|
|
||||||
|
REQUIREMENTS = ['smhi-pkg==1.0.4']
|
||||||
|
|
||||||
|
DEFAULT_NAME = 'smhi'
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
|
||||||
|
"""Set up configured smhi."""
|
||||||
|
# We allow setup only through config flow type of config
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up smhi forecast as config entry."""
|
||||||
|
hass.async_create_task(hass.config_entries.async_forward_entry_setup(
|
||||||
|
config_entry, 'weather'))
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry) -> bool:
|
||||||
|
"""Unload a config entry."""
|
||||||
|
await hass.config_entries.async_forward_entry_unload(
|
||||||
|
config_entry, 'weather')
|
||||||
|
return True
|
124
homeassistant/components/smhi/config_flow.py
Normal file
124
homeassistant/components/smhi/config_flow.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
"""Config flow to configure smhi component.
|
||||||
|
|
||||||
|
First time the user creates the configuration and
|
||||||
|
a valid location is set in the hass configuration yaml
|
||||||
|
it will use that location and use it as default values.
|
||||||
|
|
||||||
|
Additional locations can be added in config form.
|
||||||
|
The input location will be checked by invoking
|
||||||
|
the API. Exception will be thrown if the location
|
||||||
|
is not supported by the API (Swedish locations only)
|
||||||
|
"""
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant import config_entries, data_entry_flow
|
||||||
|
from homeassistant.const import (CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME)
|
||||||
|
from homeassistant.core import HomeAssistant, callback
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
|
from .const import DOMAIN, HOME_LOCATION_NAME
|
||||||
|
|
||||||
|
REQUIREMENTS = ['smhi-pkg==1.0.4']
|
||||||
|
|
||||||
|
|
||||||
|
@callback
|
||||||
|
def smhi_locations(hass: HomeAssistant):
|
||||||
|
"""Return configurations of SMHI component."""
|
||||||
|
return set((slugify(entry.data[CONF_NAME])) for
|
||||||
|
entry in hass.config_entries.async_entries(DOMAIN))
|
||||||
|
|
||||||
|
|
||||||
|
@config_entries.HANDLERS.register(DOMAIN)
|
||||||
|
class SmhiFlowHandler(data_entry_flow.FlowHandler):
|
||||||
|
"""Config flow for SMHI component."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
"""Initialize SMHI forecast configuration flow."""
|
||||||
|
self._errors = {}
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
self._errors = {}
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
is_ok = await self._check_location(
|
||||||
|
user_input[CONF_LONGITUDE],
|
||||||
|
user_input[CONF_LATITUDE]
|
||||||
|
)
|
||||||
|
if is_ok:
|
||||||
|
name = slugify(user_input[CONF_NAME])
|
||||||
|
if not self._name_in_configuration_exists(name):
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_NAME],
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._errors[CONF_NAME] = 'name_exists'
|
||||||
|
else:
|
||||||
|
self._errors['base'] = 'wrong_location'
|
||||||
|
|
||||||
|
# If hass config has the location set and
|
||||||
|
# is a valid coordinate the default location
|
||||||
|
# is set as default values in the form
|
||||||
|
if not smhi_locations(self.hass):
|
||||||
|
if await self._homeassistant_location_exists():
|
||||||
|
return await self._show_config_form(
|
||||||
|
name=HOME_LOCATION_NAME,
|
||||||
|
latitude=self.hass.config.latitude,
|
||||||
|
longitude=self.hass.config.longitude
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self._show_config_form()
|
||||||
|
|
||||||
|
async def _homeassistant_location_exists(self) -> bool:
|
||||||
|
"""Return true if default location is set and is valid."""
|
||||||
|
if self.hass.config.latitude != 0.0 and \
|
||||||
|
self.hass.config.longitude != 0.0:
|
||||||
|
# Return true if valid location
|
||||||
|
if await self._check_location(
|
||||||
|
self.hass.config.longitude,
|
||||||
|
self.hass.config.latitude):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _name_in_configuration_exists(self, name: str) -> bool:
|
||||||
|
"""Return True if name exists in configuration."""
|
||||||
|
if name in smhi_locations(self.hass):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _show_config_form(self,
|
||||||
|
name: str = None,
|
||||||
|
latitude: str = None,
|
||||||
|
longitude: str = None):
|
||||||
|
"""Show the configuration form to edit location data."""
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id='user',
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=name): str,
|
||||||
|
vol.Required(CONF_LATITUDE, default=latitude): cv.latitude,
|
||||||
|
vol.Required(CONF_LONGITUDE, default=longitude): cv.longitude
|
||||||
|
}),
|
||||||
|
errors=self._errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _check_location(self, longitude: str, latitude: str) -> bool:
|
||||||
|
"""Return true if location is ok."""
|
||||||
|
from smhi.smhi_lib import Smhi, SmhiForecastException
|
||||||
|
try:
|
||||||
|
session = aiohttp_client.async_get_clientsession(self.hass)
|
||||||
|
smhi_api = Smhi(longitude, latitude, session=session)
|
||||||
|
|
||||||
|
await smhi_api.async_get_forecast()
|
||||||
|
|
||||||
|
return True
|
||||||
|
except SmhiForecastException:
|
||||||
|
# The API will throw an exception if faulty location
|
||||||
|
pass
|
||||||
|
|
||||||
|
return False
|
12
homeassistant/components/smhi/const.py
Normal file
12
homeassistant/components/smhi/const.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
"""Constants in smhi component."""
|
||||||
|
import logging
|
||||||
|
from homeassistant.components.weather import DOMAIN as WEATHER_DOMAIN
|
||||||
|
|
||||||
|
HOME_LOCATION_NAME = 'Home'
|
||||||
|
|
||||||
|
ATTR_SMHI_CLOUDINESS = 'cloudiness'
|
||||||
|
DOMAIN = 'smhi'
|
||||||
|
LOGGER = logging.getLogger('homeassistant.components.smhi')
|
||||||
|
ENTITY_ID_SENSOR_FORMAT = WEATHER_DOMAIN + ".smhi_{}"
|
||||||
|
ENTITY_ID_SENSOR_FORMAT_HOME = ENTITY_ID_SENSOR_FORMAT.format(
|
||||||
|
HOME_LOCATION_NAME)
|
19
homeassistant/components/smhi/strings.json
Normal file
19
homeassistant/components/smhi/strings.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"title": "Swedish weather service (SMHI)",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Location in Sweden",
|
||||||
|
"data": {
|
||||||
|
"name": "Name",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"longitude": "Longitude"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"name_exists": "Name already exists",
|
||||||
|
"wrong_location": "Location Sweden only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -40,12 +40,21 @@ ATTR_WEATHER_WIND_SPEED = 'wind_speed'
|
||||||
|
|
||||||
async def async_setup(hass, config):
|
async def async_setup(hass, config):
|
||||||
"""Set up the weather component."""
|
"""Set up the weather component."""
|
||||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
component = hass.data[DOMAIN] = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||||
|
|
||||||
await component.async_setup(config)
|
await component.async_setup(config)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, entry):
|
||||||
|
"""Set up a config entry."""
|
||||||
|
return await hass.data[DOMAIN].async_setup_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass, entry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
return await hass.data[DOMAIN].async_unload_entry(entry)
|
||||||
|
|
||||||
|
|
||||||
class WeatherEntity(Entity):
|
class WeatherEntity(Entity):
|
||||||
"""ABC for weather data."""
|
"""ABC for weather data."""
|
||||||
|
|
||||||
|
|
243
homeassistant/components/weather/smhi.py
Normal file
243
homeassistant/components/weather/smhi.py
Normal file
|
@ -0,0 +1,243 @@
|
||||||
|
"""Support for the Swedish weather institute weather service.
|
||||||
|
|
||||||
|
For more details about this platform, please refer to the documentation
|
||||||
|
https://home-assistant.io/components/weather.smhi/
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import timedelta
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
import async_timeout
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_LATITUDE, CONF_LONGITUDE,
|
||||||
|
CONF_NAME, TEMP_CELSIUS)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
from homeassistant.util import dt, Throttle
|
||||||
|
|
||||||
|
from homeassistant.components.weather import (
|
||||||
|
WeatherEntity, ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP,
|
||||||
|
ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME,
|
||||||
|
ATTR_FORECAST_PRECIPITATION)
|
||||||
|
|
||||||
|
from homeassistant.components.smhi.const import (
|
||||||
|
ENTITY_ID_SENSOR_FORMAT, ATTR_SMHI_CLOUDINESS)
|
||||||
|
|
||||||
|
DEPENDENCIES = ['smhi']
|
||||||
|
REQUIREMENTS = ['smhi-pkg==1.0.4']
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Used to map condition from API results
|
||||||
|
CONDITION_CLASSES = {
|
||||||
|
'cloudy': [5, 6],
|
||||||
|
'fog': [7],
|
||||||
|
'hail': [],
|
||||||
|
'lightning': [21],
|
||||||
|
'lightning-rainy': [11],
|
||||||
|
'partlycloudy': [3, 4],
|
||||||
|
'pouring': [10, 20],
|
||||||
|
'rainy': [8, 9, 18, 19],
|
||||||
|
'snowy': [15, 16, 17, 25, 26, 27],
|
||||||
|
'snowy-rainy': [12, 13, 14, 22, 23, 24],
|
||||||
|
'sunny': [1, 2],
|
||||||
|
'windy': [],
|
||||||
|
'windy-variant': [],
|
||||||
|
'exceptional': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 5 minutes between retrying connect to API again
|
||||||
|
RETRY_TIMEOUT = 5*60
|
||||||
|
|
||||||
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=31)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_platform(hass, config, async_add_entities,
|
||||||
|
discovery_info=None):
|
||||||
|
"""Old way of setting up components.
|
||||||
|
|
||||||
|
Can only be called when a user accidentally mentions smhi in the
|
||||||
|
config. In that case it will be ignored.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant,
|
||||||
|
config_entry: ConfigEntry,
|
||||||
|
config_entries) -> bool:
|
||||||
|
"""Add a weather entity from map location."""
|
||||||
|
location = config_entry.data
|
||||||
|
name = location[CONF_NAME]
|
||||||
|
|
||||||
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
|
||||||
|
entity = SmhiWeather(name, location[CONF_LATITUDE],
|
||||||
|
location[CONF_LONGITUDE],
|
||||||
|
session=session)
|
||||||
|
entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name)
|
||||||
|
|
||||||
|
config_entries([entity], True)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class SmhiWeather(WeatherEntity):
|
||||||
|
"""Representation of a weather entity."""
|
||||||
|
|
||||||
|
def __init__(self, name: str, latitude: str,
|
||||||
|
longitude: str,
|
||||||
|
session: aiohttp.ClientSession = None) -> None:
|
||||||
|
"""Initialize the SMHI weather entity."""
|
||||||
|
from smhi import Smhi
|
||||||
|
|
||||||
|
self._name = name
|
||||||
|
self._latitude = latitude
|
||||||
|
self._longitude = longitude
|
||||||
|
self._forecasts = None
|
||||||
|
self._fail_count = 0
|
||||||
|
self._smhi_api = Smhi(self._longitude, self._latitude,
|
||||||
|
session=session)
|
||||||
|
|
||||||
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Refresh the forecast data from SMHI weather API."""
|
||||||
|
from smhi.smhi_lib import SmhiForecastException
|
||||||
|
|
||||||
|
def fail():
|
||||||
|
self._fail_count += 1
|
||||||
|
if self._fail_count < 3:
|
||||||
|
self.hass.helpers.event.async_call_later(
|
||||||
|
RETRY_TIMEOUT, self.retry_update())
|
||||||
|
|
||||||
|
try:
|
||||||
|
with async_timeout.timeout(10, loop=self.hass.loop):
|
||||||
|
self._forecasts = await self.get_weather_forecast()
|
||||||
|
self._fail_count = 0
|
||||||
|
|
||||||
|
except (asyncio.TimeoutError, SmhiForecastException):
|
||||||
|
_LOGGER.error("Failed to connect to SMHI API, "
|
||||||
|
"retry in 5 minutes")
|
||||||
|
fail()
|
||||||
|
|
||||||
|
async def retry_update(self):
|
||||||
|
"""Retry refresh weather forecast."""
|
||||||
|
self.async_update()
|
||||||
|
|
||||||
|
async def get_weather_forecast(self) -> []:
|
||||||
|
"""Return the current forecasts from SMHI API."""
|
||||||
|
return await self._smhi_api.async_get_forecast()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the sensor."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature(self) -> int:
|
||||||
|
"""Return the temperature."""
|
||||||
|
if self._forecasts is not None:
|
||||||
|
return self._forecasts[0].temperature
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def temperature_unit(self) -> str:
|
||||||
|
"""Return the unit of measurement."""
|
||||||
|
return TEMP_CELSIUS
|
||||||
|
|
||||||
|
@property
|
||||||
|
def humidity(self) -> int:
|
||||||
|
"""Return the humidity."""
|
||||||
|
if self._forecasts is not None:
|
||||||
|
return self._forecasts[0].humidity
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wind_speed(self) -> float:
|
||||||
|
"""Return the wind speed."""
|
||||||
|
if self._forecasts is not None:
|
||||||
|
# Convert from m/s to km/h
|
||||||
|
return round(self._forecasts[0].wind_speed*18/5)
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def wind_bearing(self) -> int:
|
||||||
|
"""Return the wind bearing."""
|
||||||
|
if self._forecasts is not None:
|
||||||
|
return self._forecasts[0].wind_direction
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def visibility(self) -> float:
|
||||||
|
"""Return the visibility."""
|
||||||
|
if self._forecasts is not None:
|
||||||
|
return self._forecasts[0].horizontal_visibility
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pressure(self) -> int:
|
||||||
|
"""Return the pressure."""
|
||||||
|
if self._forecasts is not None:
|
||||||
|
return self._forecasts[0].pressure
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cloudiness(self) -> int:
|
||||||
|
"""Return the cloudiness."""
|
||||||
|
if self._forecasts is not None:
|
||||||
|
return self._forecasts[0].cloudiness
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def condition(self) -> str:
|
||||||
|
"""Return the weather condition."""
|
||||||
|
if self._forecasts is None:
|
||||||
|
return None
|
||||||
|
return next((
|
||||||
|
k for k, v in CONDITION_CLASSES.items()
|
||||||
|
if self._forecasts[0].symbol in v), None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attribution(self) -> str:
|
||||||
|
"""Return the attribution."""
|
||||||
|
return 'Swedish weather institute (SMHI)'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def forecast(self) -> List:
|
||||||
|
"""Return the forecast."""
|
||||||
|
if self._forecasts is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for forecast in self._forecasts:
|
||||||
|
condition = next((
|
||||||
|
k for k, v in CONDITION_CLASSES.items()
|
||||||
|
if forecast.symbol in v), None)
|
||||||
|
|
||||||
|
# Only get mid day forecasts
|
||||||
|
if forecast.valid_time.hour == 12:
|
||||||
|
data.append({
|
||||||
|
ATTR_FORECAST_TIME:
|
||||||
|
dt.as_local(forecast.valid_time),
|
||||||
|
ATTR_FORECAST_TEMP:
|
||||||
|
forecast.temperature_max,
|
||||||
|
ATTR_FORECAST_TEMP_LOW:
|
||||||
|
forecast.temperature_min,
|
||||||
|
ATTR_FORECAST_PRECIPITATION:
|
||||||
|
round(forecast.mean_precipitation*24),
|
||||||
|
ATTR_FORECAST_CONDITION:
|
||||||
|
condition
|
||||||
|
})
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_state_attributes(self) -> Dict:
|
||||||
|
"""Return SMHI specific attributes."""
|
||||||
|
if self.cloudiness:
|
||||||
|
return {ATTR_SMHI_CLOUDINESS: self.cloudiness}
|
|
@ -146,6 +146,7 @@ FLOWS = [
|
||||||
'mqtt',
|
'mqtt',
|
||||||
'nest',
|
'nest',
|
||||||
'openuv',
|
'openuv',
|
||||||
|
'smhi',
|
||||||
'sonos',
|
'sonos',
|
||||||
'tradfri',
|
'tradfri',
|
||||||
'zone',
|
'zone',
|
||||||
|
|
|
@ -1366,6 +1366,11 @@ smappy==0.2.16
|
||||||
# homeassistant.components.sensor.htu21d
|
# homeassistant.components.sensor.htu21d
|
||||||
# smbus-cffi==0.5.1
|
# smbus-cffi==0.5.1
|
||||||
|
|
||||||
|
# homeassistant.components.smhi
|
||||||
|
# homeassistant.components.smhi.config_flow
|
||||||
|
# homeassistant.components.weather.smhi
|
||||||
|
smhi-pkg==1.0.4
|
||||||
|
|
||||||
# homeassistant.components.media_player.snapcast
|
# homeassistant.components.media_player.snapcast
|
||||||
snapcast==2.0.8
|
snapcast==2.0.8
|
||||||
|
|
||||||
|
|
|
@ -211,6 +211,11 @@ rxv==0.5.1
|
||||||
# homeassistant.components.sleepiq
|
# homeassistant.components.sleepiq
|
||||||
sleepyq==0.6
|
sleepyq==0.6
|
||||||
|
|
||||||
|
# homeassistant.components.smhi
|
||||||
|
# homeassistant.components.smhi.config_flow
|
||||||
|
# homeassistant.components.weather.smhi
|
||||||
|
smhi-pkg==1.0.4
|
||||||
|
|
||||||
# homeassistant.components.climate.honeywell
|
# homeassistant.components.climate.honeywell
|
||||||
somecomfort==0.5.2
|
somecomfort==0.5.2
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ TEST_REQUIREMENTS = (
|
||||||
'ring_doorbell',
|
'ring_doorbell',
|
||||||
'rxv',
|
'rxv',
|
||||||
'sleepyq',
|
'sleepyq',
|
||||||
|
'smhi-pkg',
|
||||||
'somecomfort',
|
'somecomfort',
|
||||||
'sqlalchemy',
|
'sqlalchemy',
|
||||||
'statsd',
|
'statsd',
|
||||||
|
|
1
tests/components/smhi/__init__.py
Normal file
1
tests/components/smhi/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the SMHI component."""
|
11
tests/components/smhi/common.py
Normal file
11
tests/components/smhi/common.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
"""Common test utilities."""
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
|
||||||
|
class AsyncMock(Mock):
|
||||||
|
"""Implements Mock async."""
|
||||||
|
|
||||||
|
# pylint: disable=W0235
|
||||||
|
async def __call__(self, *args, **kwargs):
|
||||||
|
"""Hack for async support for Mock."""
|
||||||
|
return super(AsyncMock, self).__call__(*args, **kwargs)
|
276
tests/components/smhi/test_config_flow.py
Normal file
276
tests/components/smhi/test_config_flow.py
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
"""Tests for SMHI config flow."""
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException
|
||||||
|
|
||||||
|
from tests.common import mock_coro
|
||||||
|
|
||||||
|
from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE
|
||||||
|
from homeassistant.components.smhi import config_flow
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=W0212
|
||||||
|
async def test_homeassistant_location_exists() -> None:
|
||||||
|
"""Test if homeassistant location exists it should return True."""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
with patch.object(flow, '_check_location',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
|
# Test exists
|
||||||
|
hass.config.location_name = 'Home'
|
||||||
|
hass.config.latitude = 17.8419
|
||||||
|
hass.config.longitude = 59.3262
|
||||||
|
|
||||||
|
assert await flow._homeassistant_location_exists() is True
|
||||||
|
|
||||||
|
# Test not exists
|
||||||
|
hass.config.location_name = None
|
||||||
|
hass.config.latitude = 0
|
||||||
|
hass.config.longitude = 0
|
||||||
|
|
||||||
|
assert await flow._homeassistant_location_exists() is False
|
||||||
|
|
||||||
|
|
||||||
|
async def test_name_in_configuration_exists() -> None:
|
||||||
|
"""Test if home location exists in configuration."""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
# Test exists
|
||||||
|
hass.config.location_name = 'Home'
|
||||||
|
hass.config.latitude = 17.8419
|
||||||
|
hass.config.longitude = 59.3262
|
||||||
|
|
||||||
|
# Check not exists
|
||||||
|
with patch.object(config_flow, 'smhi_locations',
|
||||||
|
return_value={
|
||||||
|
'test': 'something', 'test2': 'something else'
|
||||||
|
}):
|
||||||
|
|
||||||
|
assert flow._name_in_configuration_exists('no_exist_name') is False
|
||||||
|
|
||||||
|
# Check exists
|
||||||
|
with patch.object(config_flow, 'smhi_locations',
|
||||||
|
return_value={
|
||||||
|
'test': 'something', 'name_exist': 'config'
|
||||||
|
}):
|
||||||
|
|
||||||
|
assert flow._name_in_configuration_exists('name_exist') is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_smhi_locations(hass) -> None:
|
||||||
|
"""Test return empty set."""
|
||||||
|
locations = config_flow.smhi_locations(hass)
|
||||||
|
assert not locations
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_config_form() -> None:
|
||||||
|
"""Test show configuration form."""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow._show_config_form()
|
||||||
|
|
||||||
|
assert result['type'] == 'form'
|
||||||
|
assert result['step_id'] == 'user'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_show_config_form_default_values() -> None:
|
||||||
|
"""Test show configuration form."""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
result = await flow._show_config_form(
|
||||||
|
name="test", latitude='65', longitude='17')
|
||||||
|
|
||||||
|
assert result['type'] == 'form'
|
||||||
|
assert result['step_id'] == 'user'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_with_home_location(hass) -> None:
|
||||||
|
"""Test config flow .
|
||||||
|
|
||||||
|
Tests the flow when a default location is configured
|
||||||
|
then it should return a form with default values
|
||||||
|
"""
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
with patch.object(flow, '_check_location',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
|
hass.config.location_name = 'Home'
|
||||||
|
hass.config.latitude = 17.8419
|
||||||
|
hass.config.longitude = 59.3262
|
||||||
|
|
||||||
|
result = await flow.async_step_user()
|
||||||
|
assert result['type'] == 'form'
|
||||||
|
assert result['step_id'] == 'user'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_show_form() -> None:
|
||||||
|
"""Test show form scenarios first time.
|
||||||
|
|
||||||
|
Test when the form should show when no configurations exists
|
||||||
|
"""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
# Test show form when home assistant config exists and
|
||||||
|
# home is already configured, then new config is allowed
|
||||||
|
with \
|
||||||
|
patch.object(flow, '_show_config_form',
|
||||||
|
return_value=mock_coro()) as config_form, \
|
||||||
|
patch.object(flow, '_homeassistant_location_exists',
|
||||||
|
return_value=mock_coro(True)), \
|
||||||
|
patch.object(config_flow, 'smhi_locations',
|
||||||
|
return_value={
|
||||||
|
'test': 'something', 'name_exist': 'config'
|
||||||
|
}):
|
||||||
|
await flow.async_step_user()
|
||||||
|
assert len(config_form.mock_calls) == 1
|
||||||
|
|
||||||
|
# Test show form when home assistant config not and
|
||||||
|
# home is not configured
|
||||||
|
with \
|
||||||
|
patch.object(flow, '_show_config_form',
|
||||||
|
return_value=mock_coro()) as config_form, \
|
||||||
|
patch.object(flow, '_homeassistant_location_exists',
|
||||||
|
return_value=mock_coro(False)), \
|
||||||
|
patch.object(config_flow, 'smhi_locations',
|
||||||
|
return_value={
|
||||||
|
'test': 'something', 'name_exist': 'config'
|
||||||
|
}):
|
||||||
|
|
||||||
|
await flow.async_step_user()
|
||||||
|
assert len(config_form.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_show_form_name_exists() -> None:
|
||||||
|
"""Test show form if name already exists.
|
||||||
|
|
||||||
|
Test when the form should show when no configurations exists
|
||||||
|
"""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'}
|
||||||
|
# Test show form when home assistant config exists and
|
||||||
|
# home is already configured, then new config is allowed
|
||||||
|
with \
|
||||||
|
patch.object(flow, '_show_config_form',
|
||||||
|
return_value=mock_coro()) as config_form, \
|
||||||
|
patch.object(flow, '_name_in_configuration_exists',
|
||||||
|
return_value=True), \
|
||||||
|
patch.object(config_flow, 'smhi_locations',
|
||||||
|
return_value={
|
||||||
|
'test': 'something', 'name_exist': 'config'
|
||||||
|
}), \
|
||||||
|
patch.object(flow, '_check_location',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
|
|
||||||
|
await flow.async_step_user(user_input=test_data)
|
||||||
|
|
||||||
|
assert len(config_form.mock_calls) == 1
|
||||||
|
assert len(flow._errors) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_entry_created_from_user_input() -> None:
|
||||||
|
"""Test that create data from user input.
|
||||||
|
|
||||||
|
Test when the form should show when no configurations exists
|
||||||
|
"""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'}
|
||||||
|
|
||||||
|
# Test that entry created when user_input name not exists
|
||||||
|
with \
|
||||||
|
patch.object(flow, '_show_config_form',
|
||||||
|
return_value=mock_coro()) as config_form, \
|
||||||
|
patch.object(flow, '_name_in_configuration_exists',
|
||||||
|
return_value=False), \
|
||||||
|
patch.object(flow, '_homeassistant_location_exists',
|
||||||
|
return_value=mock_coro(False)), \
|
||||||
|
patch.object(config_flow, 'smhi_locations',
|
||||||
|
return_value={
|
||||||
|
'test': 'something', 'name_exist': 'config'
|
||||||
|
}), \
|
||||||
|
patch.object(flow, '_check_location',
|
||||||
|
return_value=mock_coro(True)):
|
||||||
|
|
||||||
|
result = await flow.async_step_user(user_input=test_data)
|
||||||
|
|
||||||
|
assert result['type'] == 'create_entry'
|
||||||
|
assert result['data'] == test_data
|
||||||
|
assert not config_form.mock_calls
|
||||||
|
|
||||||
|
|
||||||
|
async def test_flow_entry_created_user_input_faulty() -> None:
|
||||||
|
"""Test that create data from user input and are faulty.
|
||||||
|
|
||||||
|
Test when the form should show when user puts faulty location
|
||||||
|
in the config gui. Then the form should show with error
|
||||||
|
"""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
test_data = {'name': 'home', CONF_LONGITUDE: '0', CONF_LATITUDE: '0'}
|
||||||
|
|
||||||
|
# Test that entry created when user_input name not exists
|
||||||
|
with \
|
||||||
|
patch.object(flow, '_check_location',
|
||||||
|
return_value=mock_coro(True)), \
|
||||||
|
patch.object(flow, '_show_config_form',
|
||||||
|
return_value=mock_coro()) as config_form, \
|
||||||
|
patch.object(flow, '_name_in_configuration_exists',
|
||||||
|
return_value=False), \
|
||||||
|
patch.object(flow, '_homeassistant_location_exists',
|
||||||
|
return_value=mock_coro(False)), \
|
||||||
|
patch.object(config_flow, 'smhi_locations',
|
||||||
|
return_value={
|
||||||
|
'test': 'something', 'name_exist': 'config'
|
||||||
|
}), \
|
||||||
|
patch.object(flow, '_check_location',
|
||||||
|
return_value=mock_coro(False)):
|
||||||
|
|
||||||
|
await flow.async_step_user(user_input=test_data)
|
||||||
|
|
||||||
|
assert len(config_form.mock_calls) == 1
|
||||||
|
assert len(flow._errors) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_location_correct() -> None:
|
||||||
|
"""Test check location when correct input."""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
with \
|
||||||
|
patch.object(config_flow.aiohttp_client, 'async_get_clientsession'),\
|
||||||
|
patch.object(SmhiApi, 'async_get_forecast',
|
||||||
|
return_value=mock_coro()):
|
||||||
|
|
||||||
|
assert await flow._check_location('58', '17') is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_check_location_faulty() -> None:
|
||||||
|
"""Test check location when faulty input."""
|
||||||
|
hass = Mock()
|
||||||
|
flow = config_flow.SmhiFlowHandler()
|
||||||
|
flow.hass = hass
|
||||||
|
|
||||||
|
with \
|
||||||
|
patch.object(config_flow.aiohttp_client,
|
||||||
|
'async_get_clientsession'), \
|
||||||
|
patch.object(SmhiApi, 'async_get_forecast',
|
||||||
|
side_effect=SmhiForecastException()):
|
||||||
|
|
||||||
|
assert await flow._check_location('58', '17') is False
|
39
tests/components/smhi/test_init.py
Normal file
39
tests/components/smhi/test_init.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
"""Test SMHI component setup process."""
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from homeassistant.components import smhi
|
||||||
|
|
||||||
|
from .common import AsyncMock
|
||||||
|
|
||||||
|
TEST_CONFIG = {
|
||||||
|
"config": {
|
||||||
|
"name": "0123456789ABCDEF",
|
||||||
|
"longitude": "62.0022",
|
||||||
|
"latitude": "17.0022"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_always_return_true() -> None:
|
||||||
|
"""Test async_setup always returns True."""
|
||||||
|
hass = Mock()
|
||||||
|
# Returns true with empty config
|
||||||
|
assert await smhi.async_setup(hass, {}) is True
|
||||||
|
|
||||||
|
# Returns true with a config provided
|
||||||
|
assert await smhi.async_setup(hass, TEST_CONFIG) is True
|
||||||
|
|
||||||
|
|
||||||
|
async def test_forward_async_setup_entry() -> None:
|
||||||
|
"""Test that it will forward setup entry."""
|
||||||
|
hass = Mock()
|
||||||
|
|
||||||
|
assert await smhi.async_setup_entry(hass, {}) is True
|
||||||
|
assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_forward_async_unload_entry() -> None:
|
||||||
|
"""Test that it will forward unload entry."""
|
||||||
|
hass = AsyncMock()
|
||||||
|
assert await smhi.async_unload_entry(hass, {}) is True
|
||||||
|
assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 1
|
292
tests/components/weather/test_smhi.py
Normal file
292
tests/components/weather/test_smhi.py
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
"""Test for the smhi weather entity."""
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from homeassistant.components.weather import (
|
||||||
|
ATTR_FORECAST_CONDITION, ATTR_FORECAST_TEMP, ATTR_FORECAST_TIME,
|
||||||
|
ATTR_WEATHER_TEMPERATURE, ATTR_WEATHER_HUMIDITY, ATTR_WEATHER_PRESSURE,
|
||||||
|
ATTR_FORECAST_TEMP_LOW, ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_ATTRIBUTION,
|
||||||
|
ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED,
|
||||||
|
ATTR_FORECAST_PRECIPITATION, smhi as weather_smhi,
|
||||||
|
DOMAIN as WEATHER_DOMAIN)
|
||||||
|
from homeassistant.const import TEMP_CELSIUS
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import load_fixture, MockConfigEntry
|
||||||
|
|
||||||
|
from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
TEST_CONFIG = {
|
||||||
|
"name": "test",
|
||||||
|
"longitude": "17.84197",
|
||||||
|
"latitude": "59.32624"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None:
|
||||||
|
"""Test for successfully setting up the smhi platform.
|
||||||
|
|
||||||
|
This test are deeper integrated with the core. Since only
|
||||||
|
config_flow is used the component are setup with
|
||||||
|
"async_forward_entry_setup". The actual result are tested
|
||||||
|
with the entity state rather than "per function" unity tests
|
||||||
|
"""
|
||||||
|
from smhi.smhi_lib import APIURL_TEMPLATE
|
||||||
|
|
||||||
|
uri = APIURL_TEMPLATE.format(
|
||||||
|
TEST_CONFIG['longitude'], TEST_CONFIG['latitude'])
|
||||||
|
api_response = load_fixture('smhi.json')
|
||||||
|
aioclient_mock.get(uri, text=api_response)
|
||||||
|
|
||||||
|
entry = MockConfigEntry(domain='smhi', data=TEST_CONFIG)
|
||||||
|
|
||||||
|
await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN)
|
||||||
|
assert aioclient_mock.call_count == 1
|
||||||
|
|
||||||
|
# Testing the actual entity state for
|
||||||
|
# deeper testing than normal unity test
|
||||||
|
state = hass.states.get('weather.smhi_test')
|
||||||
|
|
||||||
|
assert state.state == 'sunny'
|
||||||
|
assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50
|
||||||
|
assert state.attributes[ATTR_WEATHER_ATTRIBUTION].find('SMHI') >= 0
|
||||||
|
assert state.attributes[ATTR_WEATHER_HUMIDITY] == 55
|
||||||
|
assert state.attributes[ATTR_WEATHER_PRESSURE] == 1024
|
||||||
|
assert state.attributes[ATTR_WEATHER_TEMPERATURE] == 17
|
||||||
|
assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50
|
||||||
|
assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 7
|
||||||
|
assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134
|
||||||
|
_LOGGER.error(state.attributes)
|
||||||
|
assert len(state.attributes['forecast']) == 1
|
||||||
|
|
||||||
|
forecast = state.attributes['forecast'][0]
|
||||||
|
assert forecast[ATTR_FORECAST_TIME] == datetime(2018, 9, 2, 12, 0,
|
||||||
|
tzinfo=timezone.utc)
|
||||||
|
assert forecast[ATTR_FORECAST_TEMP] == 20
|
||||||
|
assert forecast[ATTR_FORECAST_TEMP_LOW] == 6
|
||||||
|
assert forecast[ATTR_FORECAST_PRECIPITATION] == 0
|
||||||
|
assert forecast[ATTR_FORECAST_CONDITION] == 'partlycloudy'
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup_plattform(hass):
|
||||||
|
"""Test that setup plattform does nothing."""
|
||||||
|
assert await weather_smhi.async_setup_platform(hass, None, None) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_properties_no_data(hass: HomeAssistant) -> None:
|
||||||
|
"""Test properties when no API data available."""
|
||||||
|
weather = weather_smhi.SmhiWeather('name', '10', '10')
|
||||||
|
weather.hass = hass
|
||||||
|
|
||||||
|
assert weather.name == 'name'
|
||||||
|
assert weather.should_poll is True
|
||||||
|
assert weather.temperature is None
|
||||||
|
assert weather.humidity is None
|
||||||
|
assert weather.wind_speed is None
|
||||||
|
assert weather.wind_bearing is None
|
||||||
|
assert weather.visibility is None
|
||||||
|
assert weather.pressure is None
|
||||||
|
assert weather.cloudiness is None
|
||||||
|
assert weather.condition is None
|
||||||
|
assert weather.forecast is None
|
||||||
|
assert weather.temperature_unit == TEMP_CELSIUS
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=W0212
|
||||||
|
def test_properties_unknown_symbol() -> None:
|
||||||
|
"""Test behaviour when unknown symbol from API."""
|
||||||
|
hass = Mock()
|
||||||
|
data = Mock()
|
||||||
|
data.temperature = 5
|
||||||
|
data.mean_precipitation = 1
|
||||||
|
data.humidity = 5
|
||||||
|
data.wind_speed = 10
|
||||||
|
data.wind_direction = 180
|
||||||
|
data.horizontal_visibility = 6
|
||||||
|
data.pressure = 1008
|
||||||
|
data.cloudiness = 52
|
||||||
|
data.symbol = 100 # Faulty symbol
|
||||||
|
data.valid_time = datetime(2018, 1, 1, 0, 1, 2)
|
||||||
|
|
||||||
|
data2 = Mock()
|
||||||
|
data2.temperature = 5
|
||||||
|
data2.mean_precipitation = 1
|
||||||
|
data2.humidity = 5
|
||||||
|
data2.wind_speed = 10
|
||||||
|
data2.wind_direction = 180
|
||||||
|
data2.horizontal_visibility = 6
|
||||||
|
data2.pressure = 1008
|
||||||
|
data2.cloudiness = 52
|
||||||
|
data2.symbol = 100 # Faulty symbol
|
||||||
|
data2.valid_time = datetime(2018, 1, 1, 12, 1, 2)
|
||||||
|
|
||||||
|
data3 = Mock()
|
||||||
|
data3.temperature = 5
|
||||||
|
data3.mean_precipitation = 1
|
||||||
|
data3.humidity = 5
|
||||||
|
data3.wind_speed = 10
|
||||||
|
data3.wind_direction = 180
|
||||||
|
data3.horizontal_visibility = 6
|
||||||
|
data3.pressure = 1008
|
||||||
|
data3.cloudiness = 52
|
||||||
|
data3.symbol = 100 # Faulty symbol
|
||||||
|
data3.valid_time = datetime(2018, 1, 2, 12, 1, 2)
|
||||||
|
|
||||||
|
testdata = [
|
||||||
|
data,
|
||||||
|
data2,
|
||||||
|
data3
|
||||||
|
]
|
||||||
|
|
||||||
|
weather = weather_smhi.SmhiWeather('name', '10', '10')
|
||||||
|
weather.hass = hass
|
||||||
|
weather._forecasts = testdata
|
||||||
|
assert weather.condition is None
|
||||||
|
forecast = weather.forecast[0]
|
||||||
|
assert forecast[ATTR_FORECAST_CONDITION] is None
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=W0212
|
||||||
|
async def test_refresh_weather_forecast_exceeds_retries(hass) -> None:
|
||||||
|
"""Test the refresh weather forecast function."""
|
||||||
|
from smhi.smhi_lib import SmhiForecastException
|
||||||
|
|
||||||
|
with \
|
||||||
|
patch.object(hass.helpers.event, 'async_call_later') as call_later, \
|
||||||
|
patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast',
|
||||||
|
side_effect=SmhiForecastException()):
|
||||||
|
|
||||||
|
weather = weather_smhi.SmhiWeather(
|
||||||
|
'name', '17.0022', '62.0022')
|
||||||
|
weather.hass = hass
|
||||||
|
weather._fail_count = 2
|
||||||
|
|
||||||
|
await weather.async_update()
|
||||||
|
assert weather._forecasts is None
|
||||||
|
assert not call_later.mock_calls
|
||||||
|
|
||||||
|
|
||||||
|
async def test_refresh_weather_forecast_timeout(hass) -> None:
|
||||||
|
"""Test timeout exception."""
|
||||||
|
weather = weather_smhi.SmhiWeather(
|
||||||
|
'name', '17.0022', '62.0022')
|
||||||
|
weather.hass = hass
|
||||||
|
|
||||||
|
with \
|
||||||
|
patch.object(hass.helpers.event, 'async_call_later') as call_later, \
|
||||||
|
patch.object(weather_smhi.SmhiWeather, 'retry_update'), \
|
||||||
|
patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast',
|
||||||
|
side_effect=asyncio.TimeoutError):
|
||||||
|
|
||||||
|
await weather.async_update()
|
||||||
|
assert len(call_later.mock_calls) == 1
|
||||||
|
# Assert we are going to wait RETRY_TIMEOUT seconds
|
||||||
|
assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_refresh_weather_forecast_exception() -> None:
|
||||||
|
"""Test any exception."""
|
||||||
|
from smhi.smhi_lib import SmhiForecastException
|
||||||
|
|
||||||
|
hass = Mock()
|
||||||
|
weather = weather_smhi.SmhiWeather(
|
||||||
|
'name', '17.0022', '62.0022')
|
||||||
|
weather.hass = hass
|
||||||
|
|
||||||
|
with \
|
||||||
|
patch.object(hass.helpers.event, 'async_call_later') as call_later, \
|
||||||
|
patch.object(weather_smhi, 'async_timeout'), \
|
||||||
|
patch.object(weather_smhi.SmhiWeather, 'retry_update'), \
|
||||||
|
patch.object(weather_smhi.SmhiWeather, 'get_weather_forecast',
|
||||||
|
side_effect=SmhiForecastException()):
|
||||||
|
|
||||||
|
hass.async_add_job = Mock()
|
||||||
|
call_later = hass.helpers.event.async_call_later
|
||||||
|
|
||||||
|
await weather.async_update()
|
||||||
|
assert len(call_later.mock_calls) == 1
|
||||||
|
# Assert we are going to wait RETRY_TIMEOUT seconds
|
||||||
|
assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT
|
||||||
|
|
||||||
|
|
||||||
|
async def test_retry_update():
|
||||||
|
"""Test retry function of refresh forecast."""
|
||||||
|
hass = Mock()
|
||||||
|
weather = weather_smhi.SmhiWeather(
|
||||||
|
'name', '17.0022', '62.0022')
|
||||||
|
weather.hass = hass
|
||||||
|
|
||||||
|
with patch.object(weather_smhi.SmhiWeather,
|
||||||
|
'async_update') as update:
|
||||||
|
await weather.retry_update()
|
||||||
|
assert len(update.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_condition_class():
|
||||||
|
"""Test condition class."""
|
||||||
|
def get_condition(index: int) -> str:
|
||||||
|
"""Return condition given index."""
|
||||||
|
return [k for k, v in weather_smhi.CONDITION_CLASSES.items()
|
||||||
|
if index in v][0]
|
||||||
|
|
||||||
|
# SMHI definitions as follows, see
|
||||||
|
# http://opendata.smhi.se/apidocs/metfcst/parameters.html
|
||||||
|
|
||||||
|
# 1. Clear sky
|
||||||
|
assert get_condition(1) == 'sunny'
|
||||||
|
# 2. Nearly clear sky
|
||||||
|
assert get_condition(2) == 'sunny'
|
||||||
|
# 3. Variable cloudiness
|
||||||
|
assert get_condition(3) == 'partlycloudy'
|
||||||
|
# 4. Halfclear sky
|
||||||
|
assert get_condition(4) == 'partlycloudy'
|
||||||
|
# 5. Cloudy sky
|
||||||
|
assert get_condition(5) == 'cloudy'
|
||||||
|
# 6. Overcast
|
||||||
|
assert get_condition(6) == 'cloudy'
|
||||||
|
# 7. Fog
|
||||||
|
assert get_condition(7) == 'fog'
|
||||||
|
# 8. Light rain showers
|
||||||
|
assert get_condition(8) == 'rainy'
|
||||||
|
# 9. Moderate rain showers
|
||||||
|
assert get_condition(9) == 'rainy'
|
||||||
|
# 18. Light rain
|
||||||
|
assert get_condition(18) == 'rainy'
|
||||||
|
# 19. Moderate rain
|
||||||
|
assert get_condition(19) == 'rainy'
|
||||||
|
# 10. Heavy rain showers
|
||||||
|
assert get_condition(10) == 'pouring'
|
||||||
|
# 20. Heavy rain
|
||||||
|
assert get_condition(20) == 'pouring'
|
||||||
|
# 21. Thunder
|
||||||
|
assert get_condition(21) == 'lightning'
|
||||||
|
# 11. Thunderstorm
|
||||||
|
assert get_condition(11) == 'lightning-rainy'
|
||||||
|
# 15. Light snow showers
|
||||||
|
assert get_condition(15) == 'snowy'
|
||||||
|
# 16. Moderate snow showers
|
||||||
|
assert get_condition(16) == 'snowy'
|
||||||
|
# 17. Heavy snow showers
|
||||||
|
assert get_condition(17) == 'snowy'
|
||||||
|
# 25. Light snowfall
|
||||||
|
assert get_condition(25) == 'snowy'
|
||||||
|
# 26. Moderate snowfall
|
||||||
|
assert get_condition(26) == 'snowy'
|
||||||
|
# 27. Heavy snowfall
|
||||||
|
assert get_condition(27) == 'snowy'
|
||||||
|
# 12. Light sleet showers
|
||||||
|
assert get_condition(12) == 'snowy-rainy'
|
||||||
|
# 13. Moderate sleet showers
|
||||||
|
assert get_condition(13) == 'snowy-rainy'
|
||||||
|
# 14. Heavy sleet showers
|
||||||
|
assert get_condition(14) == 'snowy-rainy'
|
||||||
|
# 22. Light sleet
|
||||||
|
assert get_condition(22) == 'snowy-rainy'
|
||||||
|
# 23. Moderate sleet
|
||||||
|
assert get_condition(23) == 'snowy-rainy'
|
||||||
|
# 24. Heavy sleet
|
||||||
|
assert get_condition(24) == 'snowy-rainy'
|
1599
tests/fixtures/smhi.json
vendored
Normal file
1599
tests/fixtures/smhi.json
vendored
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue