Home Assistant Unofficial Reference 2024.12.1
weather.py
Go to the documentation of this file.
1 """Support for the Swedish weather institute weather service."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections.abc import Mapping
7 from datetime import datetime, timedelta
8 import logging
9 from typing import Any, Final
10 
11 import aiohttp
12 from smhi import Smhi
13 from smhi.smhi_lib import SmhiForecast, SmhiForecastException
14 
16  ATTR_CONDITION_CLEAR_NIGHT,
17  ATTR_CONDITION_CLOUDY,
18  ATTR_CONDITION_EXCEPTIONAL,
19  ATTR_CONDITION_FOG,
20  ATTR_CONDITION_HAIL,
21  ATTR_CONDITION_LIGHTNING,
22  ATTR_CONDITION_LIGHTNING_RAINY,
23  ATTR_CONDITION_PARTLYCLOUDY,
24  ATTR_CONDITION_POURING,
25  ATTR_CONDITION_RAINY,
26  ATTR_CONDITION_SNOWY,
27  ATTR_CONDITION_SNOWY_RAINY,
28  ATTR_CONDITION_SUNNY,
29  ATTR_CONDITION_WINDY,
30  ATTR_CONDITION_WINDY_VARIANT,
31  ATTR_FORECAST_CLOUD_COVERAGE,
32  ATTR_FORECAST_CONDITION,
33  ATTR_FORECAST_HUMIDITY,
34  ATTR_FORECAST_NATIVE_PRECIPITATION,
35  ATTR_FORECAST_NATIVE_PRESSURE,
36  ATTR_FORECAST_NATIVE_TEMP,
37  ATTR_FORECAST_NATIVE_TEMP_LOW,
38  ATTR_FORECAST_NATIVE_WIND_GUST_SPEED,
39  ATTR_FORECAST_NATIVE_WIND_SPEED,
40  ATTR_FORECAST_TIME,
41  ATTR_FORECAST_WIND_BEARING,
42  Forecast,
43  WeatherEntity,
44  WeatherEntityFeature,
45 )
46 from homeassistant.config_entries import ConfigEntry
47 from homeassistant.const import (
48  CONF_LATITUDE,
49  CONF_LOCATION,
50  CONF_LONGITUDE,
51  CONF_NAME,
52  UnitOfLength,
53  UnitOfPrecipitationDepth,
54  UnitOfPressure,
55  UnitOfSpeed,
56  UnitOfTemperature,
57 )
58 from homeassistant.core import HomeAssistant
59 from homeassistant.helpers import aiohttp_client, sun
60 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
61 from homeassistant.helpers.entity_platform import AddEntitiesCallback
62 from homeassistant.helpers.event import async_call_later
63 from homeassistant.util import Throttle, dt as dt_util, slugify
64 
65 from .const import ATTR_SMHI_THUNDER_PROBABILITY, DOMAIN, ENTITY_ID_SENSOR_FORMAT
66 
67 _LOGGER = logging.getLogger(__name__)
68 
69 # Used to map condition from API results
70 CONDITION_CLASSES: Final[dict[str, list[int]]] = {
71  ATTR_CONDITION_CLOUDY: [5, 6],
72  ATTR_CONDITION_FOG: [7],
73  ATTR_CONDITION_HAIL: [],
74  ATTR_CONDITION_LIGHTNING: [21],
75  ATTR_CONDITION_LIGHTNING_RAINY: [11],
76  ATTR_CONDITION_PARTLYCLOUDY: [3, 4],
77  ATTR_CONDITION_POURING: [10, 20],
78  ATTR_CONDITION_RAINY: [8, 9, 18, 19],
79  ATTR_CONDITION_SNOWY: [15, 16, 17, 25, 26, 27],
80  ATTR_CONDITION_SNOWY_RAINY: [12, 13, 14, 22, 23, 24],
81  ATTR_CONDITION_SUNNY: [1, 2],
82  ATTR_CONDITION_WINDY: [],
83  ATTR_CONDITION_WINDY_VARIANT: [],
84  ATTR_CONDITION_EXCEPTIONAL: [],
85 }
86 CONDITION_MAP = {
87  cond_code: cond_ha
88  for cond_ha, cond_codes in CONDITION_CLASSES.items()
89  for cond_code in cond_codes
90 }
91 
92 TIMEOUT = 10
93 # 5 minutes between retrying connect to API again
94 RETRY_TIMEOUT = 5 * 60
95 
96 MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=31)
97 
98 
100  hass: HomeAssistant,
101  config_entry: ConfigEntry,
102  async_add_entities: AddEntitiesCallback,
103 ) -> None:
104  """Add a weather entity from map location."""
105  location = config_entry.data
106  name = slugify(location[CONF_NAME])
107 
108  session = aiohttp_client.async_get_clientsession(hass)
109 
110  entity = SmhiWeather(
111  location[CONF_NAME],
112  location[CONF_LOCATION][CONF_LATITUDE],
113  location[CONF_LOCATION][CONF_LONGITUDE],
114  session=session,
115  )
116  entity.entity_id = ENTITY_ID_SENSOR_FORMAT.format(name)
117 
118  async_add_entities([entity], True)
119 
120 
122  """Representation of a weather entity."""
123 
124  _attr_attribution = "Swedish weather institute (SMHI)"
125  _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
126  _attr_native_visibility_unit = UnitOfLength.KILOMETERS
127  _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
128  _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
129  _attr_native_pressure_unit = UnitOfPressure.HPA
130 
131  _attr_has_entity_name = True
132  _attr_name = None
133  _attr_supported_features = (
134  WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
135  )
136 
137  def __init__(
138  self,
139  name: str,
140  latitude: str,
141  longitude: str,
142  session: aiohttp.ClientSession,
143  ) -> None:
144  """Initialize the SMHI weather entity."""
145  self._attr_unique_id_attr_unique_id = f"{latitude}, {longitude}"
146  self._forecast_daily_forecast_daily: list[SmhiForecast] | None = None
147  self._forecast_hourly_forecast_hourly: list[SmhiForecast] | None = None
148  self._fail_count_fail_count = 0
149  self._smhi_api_smhi_api = Smhi(longitude, latitude, session=session)
150  self._attr_device_info_attr_device_info = DeviceInfo(
151  entry_type=DeviceEntryType.SERVICE,
152  identifiers={(DOMAIN, f"{latitude}, {longitude}")},
153  manufacturer="SMHI",
154  model="v2",
155  name=name,
156  configuration_url="http://opendata.smhi.se/apidocs/metfcst/parameters.html",
157  )
158 
159  @property
160  def extra_state_attributes(self) -> Mapping[str, Any] | None:
161  """Return additional attributes."""
162  if self._forecast_daily_forecast_daily:
163  return {
164  ATTR_SMHI_THUNDER_PROBABILITY: self._forecast_daily_forecast_daily[0].thunder,
165  }
166  return None
167 
168  @Throttle(MIN_TIME_BETWEEN_UPDATES)
169  async def async_update(self) -> None:
170  """Refresh the forecast data from SMHI weather API."""
171  try:
172  async with asyncio.timeout(TIMEOUT):
173  self._forecast_daily_forecast_daily = await self._smhi_api_smhi_api.async_get_forecast()
174  self._forecast_hourly_forecast_hourly = await self._smhi_api_smhi_api.async_get_forecast_hour()
175  self._fail_count_fail_count = 0
176  except (TimeoutError, SmhiForecastException):
177  _LOGGER.error("Failed to connect to SMHI API, retry in 5 minutes")
178  self._fail_count_fail_count += 1
179  if self._fail_count_fail_count < 3:
180  async_call_later(self.hasshass, RETRY_TIMEOUT, self.retry_updateretry_update)
181  return
182 
183  if self._forecast_daily_forecast_daily:
184  self._attr_native_temperature_attr_native_temperature = self._forecast_daily_forecast_daily[0].temperature
185  self._attr_humidity_attr_humidity = self._forecast_daily_forecast_daily[0].humidity
186  self._attr_native_wind_speed_attr_native_wind_speed = self._forecast_daily_forecast_daily[0].wind_speed
187  self._attr_wind_bearing_attr_wind_bearing = self._forecast_daily_forecast_daily[0].wind_direction
188  self._attr_native_visibility_attr_native_visibility = self._forecast_daily_forecast_daily[0].horizontal_visibility
189  self._attr_native_pressure_attr_native_pressure = self._forecast_daily_forecast_daily[0].pressure
190  self._attr_native_wind_gust_speed_attr_native_wind_gust_speed = self._forecast_daily_forecast_daily[0].wind_gust
191  self._attr_cloud_coverage_attr_cloud_coverage = self._forecast_daily_forecast_daily[0].cloudiness
192  self._attr_condition_attr_condition = CONDITION_MAP.get(self._forecast_daily_forecast_daily[0].symbol)
193  if self._attr_condition_attr_condition == ATTR_CONDITION_SUNNY and not sun.is_up(
194  self.hasshass
195  ):
196  self._attr_condition_attr_condition = ATTR_CONDITION_CLEAR_NIGHT
197  await self.async_update_listenersasync_update_listeners(("daily", "hourly"))
198 
199  async def retry_update(self, _: datetime) -> None:
200  """Retry refresh weather forecast."""
201  await self.async_updateasync_update(no_throttle=True)
202 
204  self, forecast_data: list[SmhiForecast] | None
205  ) -> list[Forecast] | None:
206  """Get forecast data."""
207  if forecast_data is None or len(forecast_data) < 3:
208  return None
209 
210  data: list[Forecast] = []
211 
212  for forecast in forecast_data[1:]:
213  condition = CONDITION_MAP.get(forecast.symbol)
214  if condition == ATTR_CONDITION_SUNNY and not sun.is_up(
215  self.hasshass, forecast.valid_time.replace(tzinfo=dt_util.UTC)
216  ):
217  condition = ATTR_CONDITION_CLEAR_NIGHT
218 
219  data.append(
220  {
221  ATTR_FORECAST_TIME: forecast.valid_time.isoformat(),
222  ATTR_FORECAST_NATIVE_TEMP: forecast.temperature_max,
223  ATTR_FORECAST_NATIVE_TEMP_LOW: forecast.temperature_min,
224  ATTR_FORECAST_NATIVE_PRECIPITATION: forecast.total_precipitation,
225  ATTR_FORECAST_CONDITION: condition,
226  ATTR_FORECAST_NATIVE_PRESSURE: forecast.pressure,
227  ATTR_FORECAST_WIND_BEARING: forecast.wind_direction,
228  ATTR_FORECAST_NATIVE_WIND_SPEED: forecast.wind_speed,
229  ATTR_FORECAST_HUMIDITY: forecast.humidity,
230  ATTR_FORECAST_NATIVE_WIND_GUST_SPEED: forecast.wind_gust,
231  ATTR_FORECAST_CLOUD_COVERAGE: forecast.cloudiness,
232  }
233  )
234 
235  return data
236 
237  async def async_forecast_daily(self) -> list[Forecast] | None:
238  """Service to retrieve the daily forecast."""
239  return self._get_forecast_data_get_forecast_data(self._forecast_daily_forecast_daily)
240 
241  async def async_forecast_hourly(self) -> list[Forecast] | None:
242  """Service to retrieve the hourly forecast."""
243  return self._get_forecast_data_get_forecast_data(self._forecast_hourly_forecast_hourly)
list[Forecast]|None async_forecast_hourly(self)
Definition: weather.py:241
list[Forecast]|None _get_forecast_data(self, list[SmhiForecast]|None forecast_data)
Definition: weather.py:205
list[Forecast]|None async_forecast_daily(self)
Definition: weather.py:237
None __init__(self, str name, str latitude, str longitude, aiohttp.ClientSession session)
Definition: weather.py:143
Mapping[str, Any]|None extra_state_attributes(self)
Definition: weather.py:160
None async_update_listeners(self, Iterable[Literal["daily", "hourly", "twice_daily"]]|None forecast_types)
Definition: __init__.py:961
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: weather.py:103
CALLBACK_TYPE async_call_later(HomeAssistant hass, float|timedelta delay, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action)
Definition: event.py:1597