core/homeassistant/components/smhi/weather.py

242 lines
6.9 KiB
Python
Raw Normal View History

"""Support for the Swedish weather institute weather service."""
import asyncio
from datetime import timedelta
import logging
from typing import Dict, List
import aiohttp
import async_timeout
from homeassistant.components.weather import (
2019-07-31 19:25:30 +00:00
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_PRECIPITATION,
ATTR_FORECAST_TEMP,
ATTR_FORECAST_TEMP_LOW,
ATTR_FORECAST_TIME,
WeatherEntity,
)
from homeassistant.config_entries import ConfigEntry
2019-07-31 19:25:30 +00:00
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 Throttle, slugify
from .const import ATTR_SMHI_CLOUDINESS, ENTITY_ID_SENSOR_FORMAT
_LOGGER = logging.getLogger(__name__)
# Used to map condition from API results
CONDITION_CLASSES = {
2019-07-31 19:25:30 +00:00
"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
2019-07-31 19:25:30 +00:00
RETRY_TIMEOUT = 5 * 60
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=31)
2019-07-31 19:25:30 +00:00
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(
2019-07-31 19:25:30 +00:00
hass: HomeAssistant, config_entry: ConfigEntry, config_entries
) -> bool:
"""Add a weather entity from map location."""
location = config_entry.data
2018-10-30 09:44:07 +00:00
name = slugify(location[CONF_NAME])
session = aiohttp_client.async_get_clientsession(hass)
entity = SmhiWeather(
2019-07-31 19:25:30 +00:00
location[CONF_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."""
2019-07-31 19:25:30 +00:00
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)
2018-12-12 08:29:45 +00:00
@property
def unique_id(self) -> str:
"""Return a unique id."""
return f"{self._latitude}, {self._longitude}"
2018-12-12 08:29:45 +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():
"""Postpone updates."""
self._fail_count += 1
if self._fail_count < 3:
self.hass.helpers.event.async_call_later(
2019-07-31 19:25:30 +00:00
RETRY_TIMEOUT, self.retry_update()
)
try:
with async_timeout.timeout(10):
self._forecasts = await self.get_weather_forecast()
self._fail_count = 0
except (asyncio.TimeoutError, SmhiForecastException):
2019-07-31 19:25:30 +00:00
_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
2019-07-31 19:25:30 +00:00
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."""
2019-07-31 19:25:30 +00:00
return "Swedish weather institute (SMHI)"
@property
def forecast(self) -> List:
"""Return the forecast."""
if self._forecasts is None or len(self._forecasts) < 2:
return None
data = []
for forecast in self._forecasts[1:]:
2019-07-31 19:25:30 +00:00
condition = next(
(k for k, v in CONDITION_CLASSES.items() if forecast.symbol in v), None
)
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, 1),
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}