Home Assistant Unofficial Reference 2024.12.1
weather.py
Go to the documentation of this file.
1 """Platform for retrieving meteorological data from Environment Canada."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
8  ATTR_CONDITION_CLEAR_NIGHT,
9  ATTR_CONDITION_CLOUDY,
10  ATTR_CONDITION_FOG,
11  ATTR_CONDITION_HAIL,
12  ATTR_CONDITION_LIGHTNING_RAINY,
13  ATTR_CONDITION_PARTLYCLOUDY,
14  ATTR_CONDITION_POURING,
15  ATTR_CONDITION_RAINY,
16  ATTR_CONDITION_SNOWY,
17  ATTR_CONDITION_SNOWY_RAINY,
18  ATTR_CONDITION_SUNNY,
19  ATTR_CONDITION_WINDY,
20  ATTR_FORECAST_CONDITION,
21  ATTR_FORECAST_NATIVE_TEMP,
22  ATTR_FORECAST_NATIVE_TEMP_LOW,
23  ATTR_FORECAST_PRECIPITATION_PROBABILITY,
24  ATTR_FORECAST_TIME,
25  DOMAIN as WEATHER_DOMAIN,
26  Forecast,
27  SingleCoordinatorWeatherEntity,
28  WeatherEntityFeature,
29 )
30 from homeassistant.config_entries import ConfigEntry
31 from homeassistant.const import (
32  UnitOfLength,
33  UnitOfPressure,
34  UnitOfSpeed,
35  UnitOfTemperature,
36 )
37 from homeassistant.core import HomeAssistant, callback
38 from homeassistant.helpers import entity_registry as er
39 from homeassistant.helpers.entity_platform import AddEntitiesCallback
40 
41 from . import device_info
42 from .const import DOMAIN
43 
44 # Icon codes from http://dd.weatheroffice.ec.gc.ca/citypage_weather/
45 # docs/current_conditions_icon_code_descriptions_e.csv
46 ICON_CONDITION_MAP = {
47  ATTR_CONDITION_SUNNY: [0, 1],
48  ATTR_CONDITION_CLEAR_NIGHT: [30, 31],
49  ATTR_CONDITION_PARTLYCLOUDY: [2, 3, 4, 5, 22, 32, 33, 34, 35],
50  ATTR_CONDITION_CLOUDY: [10],
51  ATTR_CONDITION_RAINY: [6, 9, 11, 12, 28, 36],
52  ATTR_CONDITION_LIGHTNING_RAINY: [19, 39, 46, 47],
53  ATTR_CONDITION_POURING: [13],
54  ATTR_CONDITION_SNOWY_RAINY: [7, 14, 15, 27, 37],
55  ATTR_CONDITION_SNOWY: [8, 16, 17, 18, 25, 26, 38, 40],
56  ATTR_CONDITION_WINDY: [43],
57  ATTR_CONDITION_FOG: [20, 21, 23, 24, 44],
58  ATTR_CONDITION_HAIL: [26, 27],
59 }
60 
61 
63  hass: HomeAssistant,
64  config_entry: ConfigEntry,
65  async_add_entities: AddEntitiesCallback,
66 ) -> None:
67  """Add a weather entity from a config_entry."""
68  coordinator = hass.data[DOMAIN][config_entry.entry_id]["weather_coordinator"]
69  entity_registry = er.async_get(hass)
70 
71  # Remove hourly entity from legacy config entries
72  if hourly_entity_id := entity_registry.async_get_entity_id(
73  WEATHER_DOMAIN,
74  DOMAIN,
75  _calculate_unique_id(config_entry.unique_id, True),
76  ):
77  entity_registry.async_remove(hourly_entity_id)
78 
79  async_add_entities([ECWeather(coordinator)])
80 
81 
82 def _calculate_unique_id(config_entry_unique_id: str | None, hourly: bool) -> str:
83  """Calculate unique ID."""
84  return f"{config_entry_unique_id}{'-hourly' if hourly else '-daily'}"
85 
86 
88  """Representation of a weather condition."""
89 
90  _attr_has_entity_name = True
91  _attr_native_pressure_unit = UnitOfPressure.KPA
92  _attr_native_temperature_unit = UnitOfTemperature.CELSIUS
93  _attr_native_visibility_unit = UnitOfLength.KILOMETERS
94  _attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
95  _attr_supported_features = (
96  WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
97  )
98 
99  def __init__(self, coordinator):
100  """Initialize Environment Canada weather."""
101  super().__init__(coordinator)
102  self.ec_dataec_data = coordinator.ec_data
103  self._attr_attribution_attr_attribution = self.ec_dataec_data.metadata["attribution"]
104  self._attr_translation_key_attr_translation_key = "forecast"
105  self._attr_unique_id_attr_unique_id = _calculate_unique_id(
106  coordinator.config_entry.unique_id, False
107  )
108  self._attr_device_info_attr_device_info = device_info(coordinator.config_entry)
109 
110  @property
112  """Return the temperature."""
113  if (
114  temperature := self.ec_dataec_data.conditions.get("temperature", {}).get("value")
115  ) is not None:
116  return float(temperature)
117  if (
118  self.ec_dataec_data.hourly_forecasts
119  and (temperature := self.ec_dataec_data.hourly_forecasts[0].get("temperature"))
120  is not None
121  ):
122  return float(temperature)
123  return None
124 
125  @property
126  def humidity(self):
127  """Return the humidity."""
128  if self.ec_dataec_data.conditions.get("humidity", {}).get("value"):
129  return float(self.ec_dataec_data.conditions["humidity"]["value"])
130  return None
131 
132  @property
133  def native_wind_speed(self):
134  """Return the wind speed."""
135  if self.ec_dataec_data.conditions.get("wind_speed", {}).get("value"):
136  return float(self.ec_dataec_data.conditions["wind_speed"]["value"])
137  return None
138 
139  @property
140  def wind_bearing(self):
141  """Return the wind bearing."""
142  if self.ec_dataec_data.conditions.get("wind_bearing", {}).get("value"):
143  return float(self.ec_dataec_data.conditions["wind_bearing"]["value"])
144  return None
145 
146  @property
147  def native_pressure(self):
148  """Return the pressure."""
149  if self.ec_dataec_data.conditions.get("pressure", {}).get("value"):
150  return float(self.ec_dataec_data.conditions["pressure"]["value"])
151  return None
152 
153  @property
154  def native_visibility(self):
155  """Return the visibility."""
156  if self.ec_dataec_data.conditions.get("visibility", {}).get("value"):
157  return float(self.ec_dataec_data.conditions["visibility"]["value"])
158  return None
159 
160  @property
161  def condition(self):
162  """Return the weather condition."""
163  icon_code = None
164 
165  if self.ec_dataec_data.conditions.get("icon_code", {}).get("value"):
166  icon_code = self.ec_dataec_data.conditions["icon_code"]["value"]
167  elif self.ec_dataec_data.hourly_forecasts and self.ec_dataec_data.hourly_forecasts[0].get(
168  "icon_code"
169  ):
170  icon_code = self.ec_dataec_data.hourly_forecasts[0]["icon_code"]
171 
172  if icon_code:
173  return icon_code_to_condition(int(icon_code))
174  return ""
175 
176  @callback
177  def _async_forecast_daily(self) -> list[Forecast] | None:
178  """Return the daily forecast in native units."""
179  return get_forecast(self.ec_dataec_data, False)
180 
181  @callback
182  def _async_forecast_hourly(self) -> list[Forecast] | None:
183  """Return the hourly forecast in native units."""
184  return get_forecast(self.ec_dataec_data, True)
185 
186 
187 def get_forecast(ec_data, hourly) -> list[Forecast] | None:
188  """Build the forecast array."""
189  forecast_array: list[Forecast] = []
190 
191  if not hourly:
192  if not (half_days := ec_data.daily_forecasts):
193  return None
194 
195  def get_day_forecast(
196  fcst: list[dict[str, Any]],
197  ) -> Forecast:
198  high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None
199  return {
200  ATTR_FORECAST_TIME: fcst[0]["timestamp"].isoformat(),
201  ATTR_FORECAST_NATIVE_TEMP: high_temp,
202  ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]),
203  ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
204  fcst[0]["precip_probability"]
205  ),
206  ATTR_FORECAST_CONDITION: icon_code_to_condition(
207  int(fcst[0]["icon_code"])
208  ),
209  }
210 
211  i = 2 if half_days[0]["temperature_class"] == "high" else 1
212  forecast_array.append(get_day_forecast(half_days[0:i]))
213  for i in range(i, len(half_days) - 1, 2):
214  forecast_array.append(get_day_forecast(half_days[i : i + 2])) # noqa: PERF401
215 
216  else:
217  forecast_array.extend(
218  {
219  ATTR_FORECAST_TIME: hour["period"].isoformat(),
220  ATTR_FORECAST_NATIVE_TEMP: int(hour["temperature"]),
221  ATTR_FORECAST_CONDITION: icon_code_to_condition(int(hour["icon_code"])),
222  ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
223  hour["precip_probability"]
224  ),
225  }
226  for hour in ec_data.hourly_forecasts
227  )
228 
229  return forecast_array
230 
231 
232 def icon_code_to_condition(icon_code):
233  """Return the condition corresponding to an icon code."""
234  for condition, codes in ICON_CONDITION_MAP.items():
235  if icon_code in codes:
236  return condition
237  return None
DeviceInfo|None device_info(self)
Definition: entity.py:798
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: weather.py:66
list[Forecast]|None get_forecast(ec_data, hourly)
Definition: weather.py:187
str _calculate_unique_id(str|None config_entry_unique_id, bool hourly)
Definition: weather.py:82