Home Assistant Unofficial Reference 2024.12.1
weather.py
Go to the documentation of this file.
1 """Weather component that handles meteorological data for your location."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime
6 
7 from pytomorrowio.const import DAILY, FORECASTS, HOURLY, NOWCAST, WeatherCode
8 
10  ATTR_FORECAST_CONDITION,
11  ATTR_FORECAST_HUMIDITY,
12  ATTR_FORECAST_NATIVE_DEW_POINT,
13  ATTR_FORECAST_NATIVE_PRECIPITATION,
14  ATTR_FORECAST_NATIVE_TEMP,
15  ATTR_FORECAST_NATIVE_TEMP_LOW,
16  ATTR_FORECAST_NATIVE_WIND_SPEED,
17  ATTR_FORECAST_PRECIPITATION_PROBABILITY,
18  ATTR_FORECAST_TIME,
19  ATTR_FORECAST_WIND_BEARING,
20  DOMAIN as WEATHER_DOMAIN,
21  Forecast,
22  SingleCoordinatorWeatherEntity,
23  WeatherEntityFeature,
24 )
25 from homeassistant.config_entries import ConfigEntry
26 from homeassistant.const import (
27  CONF_API_KEY,
28  UnitOfLength,
29  UnitOfPrecipitationDepth,
30  UnitOfPressure,
31  UnitOfSpeed,
32  UnitOfTemperature,
33 )
34 from homeassistant.core import HomeAssistant, callback
35 from homeassistant.helpers import entity_registry as er
36 from homeassistant.helpers.entity_platform import AddEntitiesCallback
37 from homeassistant.helpers.sun import is_up
38 from homeassistant.util import dt as dt_util
39 
40 from .const import (
41  CLEAR_CONDITIONS,
42  CONDITIONS,
43  CONF_TIMESTEP,
44  DEFAULT_FORECAST_TYPE,
45  DOMAIN,
46  MAX_FORECASTS,
47  TMRW_ATTR_CONDITION,
48  TMRW_ATTR_DEW_POINT,
49  TMRW_ATTR_HUMIDITY,
50  TMRW_ATTR_OZONE,
51  TMRW_ATTR_PRECIPITATION,
52  TMRW_ATTR_PRECIPITATION_PROBABILITY,
53  TMRW_ATTR_PRESSURE,
54  TMRW_ATTR_TEMPERATURE,
55  TMRW_ATTR_TEMPERATURE_HIGH,
56  TMRW_ATTR_TEMPERATURE_LOW,
57  TMRW_ATTR_TIMESTAMP,
58  TMRW_ATTR_VISIBILITY,
59  TMRW_ATTR_WIND_DIRECTION,
60  TMRW_ATTR_WIND_SPEED,
61 )
62 from .coordinator import TomorrowioDataUpdateCoordinator
63 from .entity import TomorrowioEntity
64 
65 
67  hass: HomeAssistant,
68  config_entry: ConfigEntry,
69  async_add_entities: AddEntitiesCallback,
70 ) -> None:
71  """Set up a config entry."""
72  coordinator = hass.data[DOMAIN][config_entry.data[CONF_API_KEY]]
73  entity_registry = er.async_get(hass)
74 
75  entities = [TomorrowioWeatherEntity(config_entry, coordinator, 4, DAILY)]
76 
77  # Add hourly and nowcast entities to legacy config entries
78  for forecast_type in (HOURLY, NOWCAST):
79  if not entity_registry.async_get_entity_id(
80  WEATHER_DOMAIN,
81  DOMAIN,
82  _calculate_unique_id(config_entry.unique_id, forecast_type),
83  ):
84  continue
85  entities.append(
86  TomorrowioWeatherEntity(config_entry, coordinator, 4, forecast_type)
87  )
88 
89  async_add_entities(entities)
90 
91 
92 def _calculate_unique_id(config_entry_unique_id: str | None, forecast_type: str) -> str:
93  """Calculate unique ID."""
94  return f"{config_entry_unique_id}_{forecast_type}"
95 
96 
98  """Entity that talks to Tomorrow.io v4 API to retrieve weather data."""
99 
100  _attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
101  _attr_native_pressure_unit = UnitOfPressure.HPA
102  _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
103  _attr_native_visibility_unit = UnitOfLength.KILOMETERS
104  _attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
105  _attr_supported_features = (
106  WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
107  )
108 
109  def __init__(
110  self,
111  config_entry: ConfigEntry,
112  coordinator: TomorrowioDataUpdateCoordinator,
113  api_version: int,
114  forecast_type: str,
115  ) -> None:
116  """Initialize Tomorrow.io Weather Entity."""
117  super().__init__(config_entry, coordinator, api_version)
118  self.forecast_typeforecast_type = forecast_type
119  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = (
120  forecast_type == DEFAULT_FORECAST_TYPE
121  )
122  self._attr_name_attr_name = forecast_type.title()
123  self._attr_unique_id_attr_unique_id = _calculate_unique_id(
124  config_entry.unique_id, forecast_type
125  )
126 
128  self,
129  forecast_dt: datetime,
130  use_datetime: bool,
131  condition: int,
132  precipitation: float | None,
133  precipitation_probability: int | None,
134  temp: float | None,
135  temp_low: float | None,
136  humidity: float | None,
137  dew_point: float | None,
138  wind_direction: float | None,
139  wind_speed: float | None,
140  ) -> Forecast:
141  """Return formatted Forecast dict from Tomorrow.io forecast data."""
142  if use_datetime:
143  translated_condition = self._translate_condition_translate_condition(
144  condition, is_up(self.hasshasshass, forecast_dt)
145  )
146  else:
147  translated_condition = self._translate_condition_translate_condition(condition, True)
148 
149  return {
150  ATTR_FORECAST_TIME: forecast_dt.isoformat(),
151  ATTR_FORECAST_CONDITION: translated_condition,
152  ATTR_FORECAST_NATIVE_PRECIPITATION: precipitation,
153  ATTR_FORECAST_PRECIPITATION_PROBABILITY: precipitation_probability,
154  ATTR_FORECAST_NATIVE_TEMP: temp,
155  ATTR_FORECAST_NATIVE_TEMP_LOW: temp_low,
156  ATTR_FORECAST_HUMIDITY: humidity,
157  ATTR_FORECAST_NATIVE_DEW_POINT: dew_point,
158  ATTR_FORECAST_WIND_BEARING: wind_direction,
159  ATTR_FORECAST_NATIVE_WIND_SPEED: wind_speed,
160  }
161 
162  @staticmethod
164  condition: int | None, sun_is_up: bool = True
165  ) -> str | None:
166  """Translate Tomorrow.io condition into an HA condition."""
167  if condition is None:
168  return None
169  # We won't guard here, instead we will fail hard
170  condition = WeatherCode(condition)
171  if condition in (WeatherCode.CLEAR, WeatherCode.MOSTLY_CLEAR):
172  if sun_is_up:
173  return CLEAR_CONDITIONS["day"]
174  return CLEAR_CONDITIONS["night"]
175  return CONDITIONS[condition]
176 
177  @property
179  """Return the platform temperature."""
180  return self._get_current_property_get_current_property(TMRW_ATTR_TEMPERATURE)
181 
182  @property
183  def native_pressure(self):
184  """Return the raw pressure."""
185  return self._get_current_property_get_current_property(TMRW_ATTR_PRESSURE)
186 
187  @property
188  def humidity(self):
189  """Return the humidity."""
190  return self._get_current_property_get_current_property(TMRW_ATTR_HUMIDITY)
191 
192  @property
193  def native_wind_speed(self):
194  """Return the raw wind speed."""
195  return self._get_current_property_get_current_property(TMRW_ATTR_WIND_SPEED)
196 
197  @property
198  def wind_bearing(self):
199  """Return the wind bearing."""
200  return self._get_current_property_get_current_property(TMRW_ATTR_WIND_DIRECTION)
201 
202  @property
203  def ozone(self):
204  """Return the O3 (ozone) level."""
205  return self._get_current_property_get_current_property(TMRW_ATTR_OZONE)
206 
207  @property
208  def condition(self):
209  """Return the condition."""
210  return self._translate_condition_translate_condition(
211  self._get_current_property_get_current_property(TMRW_ATTR_CONDITION),
212  is_up(self.hasshasshass),
213  )
214 
215  @property
216  def native_visibility(self):
217  """Return the raw visibility."""
218  return self._get_current_property_get_current_property(TMRW_ATTR_VISIBILITY)
219 
220  def _forecast(self, forecast_type: str) -> list[Forecast] | None:
221  """Return the forecast."""
222  # Check if forecasts are available
223  raw_forecasts = (
224  self.coordinator.data.get(self._config_entry_config_entry.entry_id, {})
225  .get(FORECASTS, {})
226  .get(forecast_type)
227  )
228  if not raw_forecasts:
229  return None
230 
231  forecasts: list[Forecast] = []
232  max_forecasts = MAX_FORECASTS[forecast_type]
233  forecast_count = 0
234 
235  # Convert utcnow to local to be compatible with tests
236  today = dt_util.as_local(dt_util.utcnow()).date()
237 
238  # Set default values (in cases where keys don't exist), None will be
239  # returned. Override properties per forecast type as needed
240  for forecast in raw_forecasts:
241  forecast_dt = dt_util.parse_datetime(forecast[TMRW_ATTR_TIMESTAMP])
242 
243  # Throw out past data
244  if forecast_dt is None or dt_util.as_local(forecast_dt).date() < today:
245  continue
246 
247  values = forecast["values"]
248  use_datetime = True
249 
250  condition = values.get(TMRW_ATTR_CONDITION)
251  precipitation = values.get(TMRW_ATTR_PRECIPITATION)
252  precipitation_probability = values.get(TMRW_ATTR_PRECIPITATION_PROBABILITY)
253 
254  try:
255  precipitation_probability = round(precipitation_probability)
256  except TypeError:
257  precipitation_probability = None
258 
259  temp = values.get(TMRW_ATTR_TEMPERATURE_HIGH)
260  temp_low = None
261  dew_point = values.get(TMRW_ATTR_DEW_POINT)
262  humidity = values.get(TMRW_ATTR_HUMIDITY)
263 
264  wind_direction = values.get(TMRW_ATTR_WIND_DIRECTION)
265  wind_speed = values.get(TMRW_ATTR_WIND_SPEED)
266 
267  if forecast_type == DAILY:
268  use_datetime = False
269  temp_low = values.get(TMRW_ATTR_TEMPERATURE_LOW)
270  if precipitation:
271  precipitation = precipitation * 24
272  elif forecast_type == NOWCAST:
273  # Precipitation is forecasted in CONF_TIMESTEP increments but in a
274  # per hour rate, so value needs to be converted to an amount.
275  if precipitation:
276  precipitation = (
277  precipitation / 60 * self._config_entry_config_entry.options[CONF_TIMESTEP]
278  )
279 
280  forecasts.append(
281  self._forecast_dict_forecast_dict(
282  forecast_dt,
283  use_datetime,
284  condition,
285  precipitation,
286  precipitation_probability,
287  temp,
288  temp_low,
289  humidity,
290  dew_point,
291  wind_direction,
292  wind_speed,
293  )
294  )
295 
296  forecast_count += 1
297  if forecast_count == max_forecasts:
298  break
299 
300  return forecasts
301 
302  @callback
303  def _async_forecast_daily(self) -> list[Forecast] | None:
304  """Return the daily forecast in native units."""
305  return self._forecast_forecast(DAILY)
306 
307  @callback
308  def _async_forecast_hourly(self) -> list[Forecast] | None:
309  """Return the hourly forecast in native units."""
310  return self._forecast_forecast(HOURLY)
int|str|float|None _get_current_property(self, str property_name)
Definition: entity.py:39
str|None _translate_condition(int|None condition, bool sun_is_up=True)
Definition: weather.py:165
list[Forecast]|None _forecast(self, str forecast_type)
Definition: weather.py:220
None __init__(self, ConfigEntry config_entry, TomorrowioDataUpdateCoordinator coordinator, int api_version, str forecast_type)
Definition: weather.py:115
Forecast _forecast_dict(self, datetime forecast_dt, bool use_datetime, int condition, float|None precipitation, int|None precipitation_probability, float|None temp, float|None temp_low, float|None humidity, float|None dew_point, float|None wind_direction, float|None wind_speed)
Definition: weather.py:140
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
str _calculate_unique_id(str|None config_entry_unique_id, str forecast_type)
Definition: weather.py:92
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: weather.py:70
bool is_up(HomeAssistant hass, datetime.datetime|None utc_point_in_time=None)
Definition: sun.py:142