mirror of https://github.com/home-assistant/core
244 lines
8.8 KiB
Python
244 lines
8.8 KiB
Python
"""Support for the Swedish weather institute weather service."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from collections.abc import Mapping
|
|
from datetime import datetime, timedelta
|
|
import logging
|
|
from typing import Any, Final
|
|
|
|
import aiohttp
|
|
from smhi import Smhi
|
|
from smhi.smhi_lib import SmhiForecast, SmhiForecastException
|
|
|
|
from homeassistant.components.weather import (
|
|
ATTR_CONDITION_CLEAR_NIGHT,
|
|
ATTR_CONDITION_CLOUDY,
|
|
ATTR_CONDITION_EXCEPTIONAL,
|
|
ATTR_CONDITION_FOG,
|
|
ATTR_CONDITION_HAIL,
|
|
ATTR_CONDITION_LIGHTNING,
|
|
ATTR_CONDITION_LIGHTNING_RAINY,
|
|
ATTR_CONDITION_PARTLYCLOUDY,
|
|
ATTR_CONDITION_POURING,
|
|
ATTR_CONDITION_RAINY,
|
|
ATTR_CONDITION_SNOWY,
|
|
ATTR_CONDITION_SNOWY_RAINY,
|
|
ATTR_CONDITION_SUNNY,
|
|
ATTR_CONDITION_WINDY,
|
|
ATTR_CONDITION_WINDY_VARIANT,
|
|
ATTR_FORECAST_CLOUD_COVERAGE,
|
|
ATTR_FORECAST_CONDITION,
|
|
ATTR_FORECAST_HUMIDITY,
|
|
ATTR_FORECAST_NATIVE_PRECIPITATION,
|
|
ATTR_FORECAST_NATIVE_PRESSURE,
|
|
ATTR_FORECAST_NATIVE_TEMP,
|
|
ATTR_FORECAST_NATIVE_TEMP_LOW,
|
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
|
|
ATTR_FORECAST_NATIVE_WIND_SPEED,
|
|
ATTR_FORECAST_TIME,
|
|
ATTR_FORECAST_WIND_BEARING,
|
|
Forecast,
|
|
WeatherEntity,
|
|
WeatherEntityFeature,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.const import (
|
|
CONF_LATITUDE,
|
|
CONF_LOCATION,
|
|
CONF_LONGITUDE,
|
|
CONF_NAME,
|
|
UnitOfLength,
|
|
UnitOfPrecipitationDepth,
|
|
UnitOfPressure,
|
|
UnitOfSpeed,
|
|
UnitOfTemperature,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import aiohttp_client, sun
|
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
from homeassistant.helpers.event import async_call_later
|
|
from homeassistant.util import Throttle, dt as dt_util, slugify
|
|
|
|
from .const import ATTR_SMHI_THUNDER_PROBABILITY, DOMAIN, ENTITY_ID_SENSOR_FORMAT
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
# Used to map condition from API results
|
|
CONDITION_CLASSES: Final[dict[str, list[int]]] = {
|
|
ATTR_CONDITION_CLOUDY: [5, 6],
|
|
ATTR_CONDITION_FOG: [7],
|
|
ATTR_CONDITION_HAIL: [],
|
|
ATTR_CONDITION_LIGHTNING: [21],
|
|
ATTR_CONDITION_LIGHTNING_RAINY: [11],
|
|
ATTR_CONDITION_PARTLYCLOUDY: [3, 4],
|
|
ATTR_CONDITION_POURING: [10, 20],
|
|
ATTR_CONDITION_RAINY: [8, 9, 18, 19],
|
|
ATTR_CONDITION_SNOWY: [15, 16, 17, 25, 26, 27],
|
|
ATTR_CONDITION_SNOWY_RAINY: [12, 13, 14, 22, 23, 24],
|
|
ATTR_CONDITION_SUNNY: [1, 2],
|
|
ATTR_CONDITION_WINDY: [],
|
|
ATTR_CONDITION_WINDY_VARIANT: [],
|
|
ATTR_CONDITION_EXCEPTIONAL: [],
|
|
}
|
|
CONDITION_MAP = {
|
|
cond_code: cond_ha
|
|
for cond_ha, cond_codes in CONDITION_CLASSES.items()
|
|
for cond_code in cond_codes
|
|
}
|
|
|
|
TIMEOUT = 10
|
|
# 5 minutes between retrying connect to API again
|
|
RETRY_TIMEOUT = 5 * 60
|
|
|
|
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=31)
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
config_entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Add a weather entity from map location."""
|
|
location = config_entry.data
|
|
name = slugify(location[CONF_NAME])
|
|
|
|
session = aiohttp_client.async_get_clientsession(hass)
|
|
|
|
entity = SmhiWeather(
|
|
location[CONF_NAME],
|
|
location[CONF_LOCATION][CONF_LATITUDE],
|
|
location[CONF_LOCATION][CONF_LONGITUDE],
|
|
session=session,
|
|
)
|
|
entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name)
|
|
|
|
async_add_entities([entity], True)
|
|
|
|
|
|
class SmhiWeather(WeatherEntity):
|
|
"""Representation of a weather entity."""
|
|
|
|
_attr_attribution = "Swedish weather institute (SMHI)"
|
|
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
|
_attr_native_visibility_unit = UnitOfLength.KILOMETERS
|
|
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
|
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
|
|
_attr_native_pressure_unit = UnitOfPressure.HPA
|
|
|
|
_attr_has_entity_name = True
|
|
_attr_name = None
|
|
_attr_supported_features = (
|
|
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
|
)
|
|
|
|
def __init__(
|
|
self,
|
|
name: str,
|
|
latitude: str,
|
|
longitude: str,
|
|
session: aiohttp.ClientSession,
|
|
) -> None:
|
|
"""Initialize the SMHI weather entity."""
|
|
self._attr_unique_id = f"{latitude}, {longitude}"
|
|
self._forecast_daily: list[SmhiForecast] | None = None
|
|
self._forecast_hourly: list[SmhiForecast] | None = None
|
|
self._fail_count = 0
|
|
self._smhi_api = Smhi(longitude, latitude, session=session)
|
|
self._attr_device_info = DeviceInfo(
|
|
entry_type=DeviceEntryType.SERVICE,
|
|
identifiers={(DOMAIN, f"{latitude}, {longitude}")},
|
|
manufacturer="SMHI",
|
|
model="v2",
|
|
name=name,
|
|
configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html",
|
|
)
|
|
|
|
@property
|
|
def extra_state_attributes(self) -> Mapping[str, Any] | None:
|
|
"""Return additional attributes."""
|
|
if self._forecast_daily:
|
|
return {
|
|
ATTR_SMHI_THUNDER_PROBABILITY: self._forecast_daily[0].thunder,
|
|
}
|
|
return None
|
|
|
|
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
|
async def async_update(self) -> None:
|
|
"""Refresh the forecast data from SMHI weather API."""
|
|
try:
|
|
async with asyncio.timeout(TIMEOUT):
|
|
self._forecast_daily = await self._smhi_api.async_get_forecast()
|
|
self._forecast_hourly = await self._smhi_api.async_get_forecast_hour()
|
|
self._fail_count = 0
|
|
except (TimeoutError, SmhiForecastException):
|
|
_LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes")
|
|
self._fail_count += 1
|
|
if self._fail_count < 3:
|
|
async_call_later(self.hass, RETRY_TIMEOUT, self.retry_update)
|
|
return
|
|
|
|
if self._forecast_daily:
|
|
self._attr_native_temperature = self._forecast_daily[0].temperature
|
|
self._attr_humidity = self._forecast_daily[0].humidity
|
|
self._attr_native_wind_speed = self._forecast_daily[0].wind_speed
|
|
self._attr_wind_bearing = self._forecast_daily[0].wind_direction
|
|
self._attr_native_visibility = self._forecast_daily[0].horizontal_visibility
|
|
self._attr_native_pressure = self._forecast_daily[0].pressure
|
|
self._attr_native_wind_gust_speed = self._forecast_daily[0].wind_gust
|
|
self._attr_cloud_coverage = self._forecast_daily[0].cloudiness
|
|
self._attr_condition = CONDITION_MAP.get(self._forecast_daily[0].symbol)
|
|
if self._attr_condition == ATTR_CONDITION_SUNNY and not sun.is_up(
|
|
self.hass
|
|
):
|
|
self._attr_condition = ATTR_CONDITION_CLEAR_NIGHT
|
|
await self.async_update_listeners(("daily", "hourly"))
|
|
|
|
async def retry_update(self, _: datetime) -> None:
|
|
"""Retry refresh weather forecast."""
|
|
await self.async_update(no_throttle=True)
|
|
|
|
def _get_forecast_data(
|
|
self, forecast_data: list[SmhiForecast] | None
|
|
) -> list[Forecast] | None:
|
|
"""Get forecast data."""
|
|
if forecast_data is None or len(forecast_data) < 3:
|
|
return None
|
|
|
|
data: list[Forecast] = []
|
|
|
|
for forecast in forecast_data[1:]:
|
|
condition = CONDITION_MAP.get(forecast.symbol)
|
|
if condition == ATTR_CONDITION_SUNNY and not sun.is_up(
|
|
self.hass, forecast.valid_time.replace(tzinfo=dt_util.UTC)
|
|
):
|
|
condition = ATTR_CONDITION_CLEAR_NIGHT
|
|
|
|
data.append(
|
|
{
|
|
ATTR_FORECAST_TIME: forecast.valid_time.isoformat(),
|
|
ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max,
|
|
ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min,
|
|
ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation,
|
|
ATTR_FORECAST_CONDITION: condition,
|
|
ATTR_FORECAST_NATIVE_PRESSURE: forecast.pressure,
|
|
ATTR_FORECAST_WIND_BEARING: forecast.wind_direction,
|
|
ATTR_FORECAST_NATIVE_WIND_SPEED: forecast.wind_speed,
|
|
ATTR_FORECAST_HUMIDITY: forecast.humidity,
|
|
ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast.wind_gust,
|
|
ATTR_FORECAST_CLOUD_COVERAGE: forecast.cloudiness,
|
|
}
|
|
)
|
|
|
|
return data
|
|
|
|
async def async_forecast_daily(self) -> list[Forecast] | None:
|
|
"""Service to retrieve the daily forecast."""
|
|
return self._get_forecast_data(self._forecast_daily)
|
|
|
|
async def async_forecast_hourly(self) -> list[Forecast] | None:
|
|
"""Service to retrieve the hourly forecast."""
|
|
return self._get_forecast_data(self._forecast_hourly)
|