Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for Meteo-France raining forecast sensor."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from typing import Any
7 
8 from meteofrance_api.helpers import (
9  get_warning_text_status_from_indice_color,
10  readeable_phenomenoms_dict,
11 )
12 from meteofrance_api.model.forecast import Forecast
13 from meteofrance_api.model.rain import Rain
14 from meteofrance_api.model.warning import CurrentPhenomenons
15 
17  SensorDeviceClass,
18  SensorEntity,
19  SensorEntityDescription,
20  SensorStateClass,
21 )
22 from homeassistant.config_entries import ConfigEntry
23 from homeassistant.const import (
24  PERCENTAGE,
25  UV_INDEX,
26  UnitOfPrecipitationDepth,
27  UnitOfPressure,
28  UnitOfSpeed,
29  UnitOfTemperature,
30 )
31 from homeassistant.core import HomeAssistant
32 from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
33 from homeassistant.helpers.entity_platform import AddEntitiesCallback
35  CoordinatorEntity,
36  DataUpdateCoordinator,
37 )
38 from homeassistant.util import dt as dt_util
39 
40 from .const import (
41  ATTR_NEXT_RAIN_1_HOUR_FORECAST,
42  ATTR_NEXT_RAIN_DT_REF,
43  ATTRIBUTION,
44  COORDINATOR_ALERT,
45  COORDINATOR_FORECAST,
46  COORDINATOR_RAIN,
47  DOMAIN,
48  MANUFACTURER,
49  MODEL,
50 )
51 
52 
53 @dataclass(frozen=True, kw_only=True)
55  """Describes Meteo-France sensor entity."""
56 
57  data_path: str
58 
59 
60 SENSOR_TYPES: tuple[MeteoFranceSensorEntityDescription, ...] = (
62  key="pressure",
63  name="Pressure",
64  native_unit_of_measurement=UnitOfPressure.HPA,
65  device_class=SensorDeviceClass.PRESSURE,
66  state_class=SensorStateClass.MEASUREMENT,
67  entity_registry_enabled_default=False,
68  data_path="current_forecast:sea_level",
69  ),
71  key="wind_gust",
72  name="Wind gust",
73  native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
74  device_class=SensorDeviceClass.WIND_SPEED,
75  state_class=SensorStateClass.MEASUREMENT,
76  icon="mdi:weather-windy-variant",
77  entity_registry_enabled_default=False,
78  data_path="current_forecast:wind:gust",
79  ),
81  key="wind_speed",
82  name="Wind speed",
83  native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
84  device_class=SensorDeviceClass.WIND_SPEED,
85  state_class=SensorStateClass.MEASUREMENT,
86  entity_registry_enabled_default=False,
87  data_path="current_forecast:wind:speed",
88  ),
90  key="temperature",
91  name="Temperature",
92  native_unit_of_measurement=UnitOfTemperature.CELSIUS,
93  device_class=SensorDeviceClass.TEMPERATURE,
94  state_class=SensorStateClass.MEASUREMENT,
95  entity_registry_enabled_default=False,
96  data_path="current_forecast:T:value",
97  ),
99  key="uv",
100  name="UV",
101  native_unit_of_measurement=UV_INDEX,
102  icon="mdi:sunglasses",
103  data_path="today_forecast:uv",
104  ),
106  key="precipitation",
107  name="Daily precipitation",
108  native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
109  device_class=SensorDeviceClass.PRECIPITATION,
110  data_path="today_forecast:precipitation:24h",
111  ),
113  key="cloud",
114  name="Cloud cover",
115  native_unit_of_measurement=PERCENTAGE,
116  icon="mdi:weather-partly-cloudy",
117  data_path="current_forecast:clouds",
118  ),
120  key="original_condition",
121  name="Original condition",
122  entity_registry_enabled_default=False,
123  data_path="current_forecast:weather:desc",
124  ),
126  key="daily_original_condition",
127  name="Daily original condition",
128  entity_registry_enabled_default=False,
129  data_path="today_forecast:weather12H:desc",
130  ),
132  key="humidity",
133  name="Humidity",
134  native_unit_of_measurement=PERCENTAGE,
135  device_class=SensorDeviceClass.HUMIDITY,
136  state_class=SensorStateClass.MEASUREMENT,
137  data_path="current_forecast:humidity",
138  ),
139 )
140 
141 SENSOR_TYPES_RAIN: tuple[MeteoFranceSensorEntityDescription, ...] = (
143  key="next_rain",
144  name="Next rain",
145  device_class=SensorDeviceClass.TIMESTAMP,
146  data_path="",
147  ),
148 )
149 
150 SENSOR_TYPES_ALERT: tuple[MeteoFranceSensorEntityDescription, ...] = (
152  key="weather_alert",
153  name="Weather alert",
154  icon="mdi:weather-cloudy-alert",
155  data_path="",
156  ),
157 )
158 
159 SENSOR_TYPES_PROBABILITY: tuple[MeteoFranceSensorEntityDescription, ...] = (
161  key="rain_chance",
162  name="Rain chance",
163  native_unit_of_measurement=PERCENTAGE,
164  icon="mdi:weather-rainy",
165  data_path="probability_forecast:rain:3h",
166  ),
168  key="snow_chance",
169  name="Snow chance",
170  native_unit_of_measurement=PERCENTAGE,
171  icon="mdi:weather-snowy",
172  data_path="probability_forecast:snow:3h",
173  ),
175  key="freeze_chance",
176  name="Freeze chance",
177  native_unit_of_measurement=PERCENTAGE,
178  icon="mdi:snowflake",
179  data_path="probability_forecast:freezing",
180  ),
181 )
182 
183 
185  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
186 ) -> None:
187  """Set up the Meteo-France sensor platform."""
188  data = hass.data[DOMAIN][entry.entry_id]
189  coordinator_forecast: DataUpdateCoordinator[Forecast] = data[COORDINATOR_FORECAST]
190  coordinator_rain: DataUpdateCoordinator[Rain] | None = data[COORDINATOR_RAIN]
191  coordinator_alert: DataUpdateCoordinator[CurrentPhenomenons] | None = data.get(
192  COORDINATOR_ALERT
193  )
194 
195  entities: list[MeteoFranceSensor[Any]] = [
196  MeteoFranceSensor(coordinator_forecast, description)
197  for description in SENSOR_TYPES
198  ]
199  # Add rain forecast entity only if location support this feature
200  if coordinator_rain:
201  entities.extend(
202  [
203  MeteoFranceRainSensor(coordinator_rain, description)
204  for description in SENSOR_TYPES_RAIN
205  ]
206  )
207  # Add weather alert entity only if location support this feature
208  if coordinator_alert:
209  entities.extend(
210  [
211  MeteoFranceAlertSensor(coordinator_alert, description)
212  for description in SENSOR_TYPES_ALERT
213  ]
214  )
215  # Add weather probability entities only if location support this feature
216  if coordinator_forecast.data.probability_forecast:
217  entities.extend(
218  [
219  MeteoFranceSensor(coordinator_forecast, description)
220  for description in SENSOR_TYPES_PROBABILITY
221  ]
222  )
223 
224  async_add_entities(entities, False)
225 
226 
227 class MeteoFranceSensor[_DataT: Rain | Forecast | CurrentPhenomenons](
228  CoordinatorEntity[DataUpdateCoordinator[_DataT]], SensorEntity
229 ):
230  """Representation of a Meteo-France sensor."""
231 
232  entity_description: MeteoFranceSensorEntityDescription
233  _attr_attribution = ATTRIBUTION
234 
235  def __init__(
236  self,
237  coordinator: DataUpdateCoordinator[_DataT],
238  description: MeteoFranceSensorEntityDescription,
239  ) -> None:
240  """Initialize the Meteo-France sensor."""
241  super().__init__(coordinator)
242  self.entity_description = description
243  if hasattr(coordinator.data, "position"):
244  city_name = coordinator.data.position["name"]
245  self._attr_name = f"{city_name} {description.name}"
246  self._attr_unique_id = f"{coordinator.data.position['lat']},{coordinator.data.position['lon']}_{description.key}"
247 
248  @property
249  def device_info(self) -> DeviceInfo:
250  """Return the device info."""
251  assert self.platform.config_entry and self.platform.config_entry.unique_id
252  return DeviceInfo(
253  entry_type=DeviceEntryType.SERVICE,
254  identifiers={(DOMAIN, self.platform.config_entry.unique_id)},
255  manufacturer=MANUFACTURER,
256  model=MODEL,
257  name=self.coordinator.name,
258  )
259 
260  @property
261  def native_value(self):
262  """Return the state."""
263  path = self.entity_description.data_path.split(":")
264  data = getattr(self.coordinator.data, path[0])
265 
266  # Specific case for probability forecast
267  if path[0] == "probability_forecast":
268  if len(path) == 3:
269  # This is a fix compared to other entitty as first index is always null in API result for unknown reason
271  else:
272  value = data[0][path[1]]
273 
274  # General case
275  elif len(path) == 3:
276  value = data[path[1]][path[2]]
277  else:
278  value = data[path[1]]
279 
280  if self.entity_description.key in ("wind_speed", "wind_gust"):
281  # convert API wind speed from m/s to km/h
282  value = round(value * 3.6)
283  return value
284 
285 
287  """Representation of a Meteo-France rain sensor."""
288 
289  @property
290  def native_value(self):
291  """Return the state."""
292  # search first cadran with rain
293  next_rain = next(
294  (cadran for cadran in self.coordinator.data.forecast if cadran["rain"] > 1),
295  None,
296  )
297  return dt_util.utc_from_timestamp(next_rain["dt"]) if next_rain else None
298 
299  @property
300  def extra_state_attributes(self) -> dict[str, Any]:
301  """Return the state attributes."""
302  reference_dt = self.coordinator.data.forecast[0]["dt"]
303  return {
304  ATTR_NEXT_RAIN_DT_REF: dt_util.utc_from_timestamp(reference_dt).isoformat(),
305  ATTR_NEXT_RAIN_1_HOUR_FORECAST: {
306  f"{int((item['dt'] - reference_dt) / 60)} min": item["desc"]
307  for item in self.coordinator.data.forecast
308  },
309  }
310 
311 
312 class MeteoFranceAlertSensor(MeteoFranceSensor[CurrentPhenomenons]):
313  """Representation of a Meteo-France alert sensor."""
314 
315  def __init__(
316  self,
317  coordinator: DataUpdateCoordinator[CurrentPhenomenons],
318  description: MeteoFranceSensorEntityDescription,
319  ) -> None:
320  """Initialize the Meteo-France sensor."""
321  super().__init__(coordinator, description)
322  dept_code = self.coordinator.data.domain_id
323  self._attr_name_attr_name = f"{dept_code} {description.name}"
324  self._attr_unique_id_attr_unique_id = self._attr_name_attr_name
325 
326  @property
327  def native_value(self) -> str | None:
328  """Return the state."""
329  return get_warning_text_status_from_indice_color(
330  self.coordinator.data.get_domain_max_color()
331  )
332 
333  @property
335  """Return the state attributes."""
336  return {
337  **readeable_phenomenoms_dict(self.coordinator.data.phenomenons_max_colors),
338  }
339 
340 
342  probability_forecast: list, path: list
343 ) -> int | None:
344  """Search the first not None value in the first forecast elements."""
345  for forecast in probability_forecast[0:3]:
346  if forecast[path[1]][path[2]] is not None:
347  return forecast[path[1]][path[2]]
348 
349  # Default return value if no value founded
350  return None
None __init__(self, DataUpdateCoordinator[CurrentPhenomenons] coordinator, MeteoFranceSensorEntityDescription description)
Definition: sensor.py:319
None __init__(self, DataUpdateCoordinator[_DataT] coordinator, MeteoFranceSensorEntityDescription description)
Definition: sensor.py:239
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:186
int|None _find_first_probability_forecast_not_null(list probability_forecast, list path)
Definition: sensor.py:343