Home Assistant Unofficial Reference 2024.12.1
sensor.py
Go to the documentation of this file.
1 """Support for IQVIA sensors."""
2 
3 from __future__ import annotations
4 
5 from statistics import mean
6 from typing import Any, NamedTuple, cast
7 
8 import numpy as np
9 
11  SensorEntity,
12  SensorEntityDescription,
13  SensorStateClass,
14 )
15 from homeassistant.config_entries import ConfigEntry
16 from homeassistant.const import ATTR_STATE
17 from homeassistant.core import HomeAssistant, callback
18 from homeassistant.helpers.entity_platform import AddEntitiesCallback
19 
20 from .const import (
21  DOMAIN,
22  TYPE_ALLERGY_FORECAST,
23  TYPE_ALLERGY_INDEX,
24  TYPE_ALLERGY_OUTLOOK,
25  TYPE_ALLERGY_TODAY,
26  TYPE_ALLERGY_TOMORROW,
27  TYPE_ASTHMA_FORECAST,
28  TYPE_ASTHMA_INDEX,
29  TYPE_ASTHMA_TODAY,
30  TYPE_ASTHMA_TOMORROW,
31  TYPE_DISEASE_FORECAST,
32  TYPE_DISEASE_INDEX,
33  TYPE_DISEASE_TODAY,
34 )
35 from .entity import IQVIAEntity
36 
37 ATTR_ALLERGEN_AMOUNT = "allergen_amount"
38 ATTR_ALLERGEN_GENUS = "allergen_genus"
39 ATTR_ALLERGEN_NAME = "allergen_name"
40 ATTR_ALLERGEN_TYPE = "allergen_type"
41 ATTR_CITY = "city"
42 ATTR_OUTLOOK = "outlook"
43 ATTR_RATING = "rating"
44 ATTR_SEASON = "season"
45 ATTR_TREND = "trend"
46 ATTR_ZIP_CODE = "zip_code"
47 
48 API_CATEGORY_MAPPING = {
49  TYPE_ALLERGY_TODAY: TYPE_ALLERGY_INDEX,
50  TYPE_ALLERGY_TOMORROW: TYPE_ALLERGY_INDEX,
51  TYPE_ASTHMA_TODAY: TYPE_ASTHMA_INDEX,
52  TYPE_ASTHMA_TOMORROW: TYPE_ASTHMA_INDEX,
53  TYPE_DISEASE_TODAY: TYPE_DISEASE_INDEX,
54 }
55 
56 
57 class Rating(NamedTuple):
58  """Assign label to value range."""
59 
60  label: str
61  minimum: float
62  maximum: float
63 
64 
65 RATING_MAPPING: list[Rating] = [
66  Rating(label="Low", minimum=0.0, maximum=2.4),
67  Rating(label="Low/Medium", minimum=2.5, maximum=4.8),
68  Rating(label="Medium", minimum=4.9, maximum=7.2),
69  Rating(label="Medium/High", minimum=7.3, maximum=9.6),
70  Rating(label="High", minimum=9.7, maximum=12),
71 ]
72 
73 
74 TREND_FLAT = "Flat"
75 TREND_INCREASING = "Increasing"
76 TREND_SUBSIDING = "Subsiding"
77 
78 
79 FORECAST_SENSOR_DESCRIPTIONS = (
81  key=TYPE_ALLERGY_FORECAST,
82  name="Allergy index: forecasted average",
83  icon="mdi:flower",
84  ),
86  key=TYPE_ASTHMA_FORECAST,
87  name="Asthma index: forecasted average",
88  icon="mdi:flower",
89  ),
91  key=TYPE_DISEASE_FORECAST,
92  name="Cold & flu: forecasted average",
93  icon="mdi:snowflake",
94  ),
95 )
96 
97 INDEX_SENSOR_DESCRIPTIONS = (
99  key=TYPE_ALLERGY_TODAY,
100  name="Allergy index: today",
101  icon="mdi:flower",
102  state_class=SensorStateClass.MEASUREMENT,
103  ),
105  key=TYPE_ALLERGY_TOMORROW,
106  name="Allergy index: tomorrow",
107  icon="mdi:flower",
108  ),
110  key=TYPE_ASTHMA_TODAY,
111  name="Asthma index: today",
112  icon="mdi:flower",
113  state_class=SensorStateClass.MEASUREMENT,
114  ),
116  key=TYPE_ASTHMA_TOMORROW,
117  name="Asthma index: tomorrow",
118  icon="mdi:flower",
119  ),
121  key=TYPE_DISEASE_TODAY,
122  name="Cold & flu index: today",
123  icon="mdi:pill",
124  state_class=SensorStateClass.MEASUREMENT,
125  ),
126 )
127 
128 
130  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
131 ) -> None:
132  """Set up IQVIA sensors based on a config entry."""
133  sensors: list[ForecastSensor | IndexSensor] = [
135  hass.data[DOMAIN][entry.entry_id][
136  API_CATEGORY_MAPPING.get(description.key, description.key)
137  ],
138  entry,
139  description,
140  )
141  for description in FORECAST_SENSOR_DESCRIPTIONS
142  ]
143  sensors.extend(
144  [
145  IndexSensor(
146  hass.data[DOMAIN][entry.entry_id][
147  API_CATEGORY_MAPPING.get(description.key, description.key)
148  ],
149  entry,
150  description,
151  )
152  for description in INDEX_SENSOR_DESCRIPTIONS
153  ]
154  )
155 
156  async_add_entities(sensors)
157 
158 
159 @callback
160 def calculate_trend(indices: list[float]) -> str:
161  """Calculate the "moving average" of a set of indices."""
162  index_range = np.arange(0, len(indices))
163  index_array = np.array(indices)
164  linear_fit = np.polyfit(index_range, index_array, 1)
165  slope = round(linear_fit[0], 2)
166 
167  if slope > 0:
168  return TREND_INCREASING
169 
170  if slope < 0:
171  return TREND_SUBSIDING
172 
173  return TREND_FLAT
174 
175 
177  """Define sensor related to forecast data."""
178 
179  @callback
180  def update_from_latest_data(self) -> None:
181  """Update the sensor."""
182  if not self.availableavailableavailable:
183  return
184 
185  data = self.coordinator.data.get("Location", {})
186 
187  if not data.get("periods"):
188  return
189 
190  indices = [p["Index"] for p in data["periods"]]
191  average = round(mean(indices), 1)
192  [rating] = [
193  i.label for i in RATING_MAPPING if i.minimum <= average <= i.maximum
194  ]
195 
196  self._attr_native_value_attr_native_value = average
197  self._attr_extra_state_attributes_attr_extra_state_attributes.update(
198  {
199  ATTR_CITY: data["City"].title(),
200  ATTR_RATING: rating,
201  ATTR_STATE: data["State"],
202  ATTR_TREND: calculate_trend(indices),
203  ATTR_ZIP_CODE: data["ZIP"],
204  }
205  )
206 
207  if self.entity_descriptionentity_description.key == TYPE_ALLERGY_FORECAST:
208  outlook_coordinator = self.hasshasshass.data[DOMAIN][self._entry_entry.entry_id][
209  TYPE_ALLERGY_OUTLOOK
210  ]
211 
212  if not outlook_coordinator.last_update_success:
213  return
214 
215  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_OUTLOOK] = (
216  outlook_coordinator.data.get("Outlook")
217  )
218  self._attr_extra_state_attributes_attr_extra_state_attributes[ATTR_SEASON] = (
219  outlook_coordinator.data.get("Season")
220  )
221 
222 
224  """Define sensor related to indices."""
225 
226  @callback
227  def update_from_latest_data(self) -> None:
228  """Update the sensor."""
229  if not self.coordinator.last_update_success:
230  return
231 
232  try:
233  if self.entity_descriptionentity_description.key in (
234  TYPE_ALLERGY_TODAY,
235  TYPE_ALLERGY_TOMORROW,
236  TYPE_ASTHMA_TODAY,
237  TYPE_ASTHMA_TOMORROW,
238  TYPE_DISEASE_TODAY,
239  ):
240  data = self.coordinator.data.get("Location")
241  except KeyError:
242  return
243 
244  key = self.entity_descriptionentity_description.key.split("_")[-1].title()
245 
246  try:
247  period = next(p for p in data["periods"] if p["Type"] == key) # type: ignore[index]
248  except StopIteration:
249  return
250 
251  data = cast(dict[str, Any], data)
252  [rating] = [
253  i.label for i in RATING_MAPPING if i.minimum <= period["Index"] <= i.maximum
254  ]
255 
256  self._attr_extra_state_attributes_attr_extra_state_attributes.update(
257  {
258  ATTR_CITY: data["City"].title(),
259  ATTR_RATING: rating,
260  ATTR_STATE: data["State"],
261  ATTR_ZIP_CODE: data["ZIP"],
262  }
263  )
264 
265  if self.entity_descriptionentity_description.key in (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW):
266  for idx, attrs in enumerate(period["Triggers"]):
267  index = idx + 1
268  self._attr_extra_state_attributes_attr_extra_state_attributes.update(
269  {
270  f"{ATTR_ALLERGEN_GENUS}_{index}": attrs["Genus"],
271  f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"],
272  f"{ATTR_ALLERGEN_TYPE}_{index}": attrs["PlantType"],
273  }
274  )
275  elif self.entity_descriptionentity_description.key in (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW):
276  for idx, attrs in enumerate(period["Triggers"]):
277  index = idx + 1
278  self._attr_extra_state_attributes_attr_extra_state_attributes.update(
279  {
280  f"{ATTR_ALLERGEN_NAME}_{index}": attrs["Name"],
281  f"{ATTR_ALLERGEN_AMOUNT}_{index}": attrs["PPM"],
282  }
283  )
284  elif self.entity_descriptionentity_description.key == TYPE_DISEASE_TODAY:
285  for attrs in period["Triggers"]:
286  self._attr_extra_state_attributes_attr_extra_state_attributes[f"{attrs['Name'].lower()}_index"] = (
287  attrs["Index"]
288  )
289 
290  self._attr_native_value_attr_native_value = period["Index"]
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: sensor.py:131
str calculate_trend(list[float] indices)
Definition: sensor.py:160
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
float|None mean(list[float] values)
Definition: statistics.py:168