2018-11-13 09:01:14 +00:00
|
|
|
"""
|
|
|
|
Support for the Swedish weather institute weather service.
|
2018-10-08 21:54:55 +00:00
|
|
|
|
|
|
|
For more details about this platform, please refer to the documentation
|
|
|
|
https://home-assistant.io/components/weather.smhi/
|
|
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
from datetime import timedelta
|
2018-11-13 09:01:14 +00:00
|
|
|
import logging
|
2018-10-08 21:54:55 +00:00
|
|
|
from typing import Dict, List
|
|
|
|
|
|
|
|
import aiohttp
|
|
|
|
import async_timeout
|
|
|
|
|
2018-11-13 09:01:14 +00:00
|
|
|
from homeassistant.components.smhi.const import (
|
|
|
|
ATTR_SMHI_CLOUDINESS, ENTITY_ID_SENSOR_FORMAT)
|
|
|
|
from homeassistant.components.weather import (
|
|
|
|
ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP,
|
|
|
|
ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, WeatherEntity)
|
2018-10-08 21:54:55 +00:00
|
|
|
from homeassistant.config_entries import ConfigEntry
|
|
|
|
from homeassistant.const import (
|
2018-11-13 09:01:14 +00:00
|
|
|
CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, TEMP_CELSIUS)
|
2018-10-08 21:54:55 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
|
|
|
from homeassistant.helpers import aiohttp_client
|
2019-02-05 12:43:04 +00:00
|
|
|
from homeassistant.util import Throttle, slugify
|
2018-10-08 21:54:55 +00:00
|
|
|
|
|
|
|
DEPENDENCIES = ['smhi']
|
|
|
|
|
|
|
|
_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)
|
|
|
|
|
|
|
|
|
2018-11-13 09:01:14 +00:00
|
|
|
async def async_setup_platform(
|
|
|
|
hass, config, async_add_entities, discovery_info=None):
|
2018-10-08 21:54:55 +00:00
|
|
|
"""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
|
|
|
|
|
|
|
|
|
2018-11-13 09:01:14 +00:00
|
|
|
async def async_setup_entry(
|
|
|
|
hass: HomeAssistant, config_entry: ConfigEntry,
|
|
|
|
config_entries) -> bool:
|
2018-10-08 21:54:55 +00:00
|
|
|
"""Add a weather entity from map location."""
|
|
|
|
location = config_entry.data
|
2018-10-30 09:44:07 +00:00
|
|
|
name = slugify(location[CONF_NAME])
|
2018-10-08 21:54:55 +00:00
|
|
|
|
|
|
|
session = aiohttp_client.async_get_clientsession(hass)
|
|
|
|
|
2018-11-13 09:01:14 +00:00
|
|
|
entity = SmhiWeather(
|
|
|
|
location[CONF_NAME], location[CONF_LATITUDE], location[CONF_LONGITUDE],
|
|
|
|
session=session)
|
2018-10-08 21:54:55 +00:00
|
|
|
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
|
2018-11-13 09:01:14 +00:00
|
|
|
self._smhi_api = Smhi(self._longitude, self._latitude, session=session)
|
2018-10-08 21:54:55 +00:00
|
|
|
|
2018-12-12 08:29:45 +00:00
|
|
|
@property
|
|
|
|
def unique_id(self) -> str:
|
|
|
|
"""Return a unique id."""
|
|
|
|
return '{}, {}'.format(self._latitude, self._longitude)
|
|
|
|
|
2018-10-08 21:54:55 +00:00
|
|
|
@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():
|
2018-11-13 09:01:14 +00:00
|
|
|
"""Postpone updates."""
|
2018-10-08 21:54:55 +00:00
|
|
|
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):
|
2018-11-13 09:01:14 +00:00
|
|
|
_LOGGER.error(
|
|
|
|
"Failed to connect to SMHI API, retry in 5 minutes")
|
2018-10-08 21:54:55 +00:00
|
|
|
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
|
2018-11-13 09:01:14 +00:00
|
|
|
return round(self._forecasts[0].wind_speed * 18 / 5)
|
2018-10-08 21:54:55 +00:00
|
|
|
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."""
|
2019-02-05 12:43:04 +00:00
|
|
|
if self._forecasts is None or len(self._forecasts) < 2:
|
2018-10-08 21:54:55 +00:00
|
|
|
return None
|
|
|
|
|
|
|
|
data = []
|
2019-02-05 12:43:04 +00:00
|
|
|
|
|
|
|
for forecast in self._forecasts[1:]:
|
2018-10-08 21:54:55 +00:00
|
|
|
condition = next((
|
|
|
|
k for k, v in CONDITION_CLASSES.items()
|
|
|
|
if forecast.symbol in v), None)
|
|
|
|
|
2019-02-05 12:43:04 +00:00
|
|
|
data.append({
|
|
|
|
ATTR_FORECAST_TIME: forecast.valid_time.isoformat(),
|
|
|
|
ATTR_FORECAST_TEMP: forecast.temperature_max,
|
|
|
|
ATTR_FORECAST_TEMP_LOW: forecast.temperature_min,
|
|
|
|
ATTR_FORECAST_PRECIPITATION:
|
|
|
|
round(forecast.total_precipitation),
|
|
|
|
ATTR_FORECAST_CONDITION: condition,
|
|
|
|
})
|
2018-10-08 21:54:55 +00:00
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
@property
|
|
|
|
def device_state_attributes(self) -> Dict:
|
|
|
|
"""Return SMHI specific attributes."""
|
|
|
|
if self.cloudiness:
|
|
|
|
return {ATTR_SMHI_CLOUDINESS: self.cloudiness}
|