Home Assistant Unofficial Reference 2024.12.1
significant_change.py
Go to the documentation of this file.
1 """Helper to test significant Weather state changes."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature
8 from homeassistant.core import HomeAssistant, callback
10  check_absolute_change,
11  check_valid_float,
12 )
13 
14 from .const import (
15  ATTR_WEATHER_APPARENT_TEMPERATURE,
16  ATTR_WEATHER_CLOUD_COVERAGE,
17  ATTR_WEATHER_DEW_POINT,
18  ATTR_WEATHER_HUMIDITY,
19  ATTR_WEATHER_OZONE,
20  ATTR_WEATHER_PRESSURE,
21  ATTR_WEATHER_PRESSURE_UNIT,
22  ATTR_WEATHER_TEMPERATURE,
23  ATTR_WEATHER_TEMPERATURE_UNIT,
24  ATTR_WEATHER_UV_INDEX,
25  ATTR_WEATHER_VISIBILITY,
26  ATTR_WEATHER_WIND_BEARING,
27  ATTR_WEATHER_WIND_GUST_SPEED,
28  ATTR_WEATHER_WIND_SPEED,
29  ATTR_WEATHER_WIND_SPEED_UNIT,
30 )
31 
32 SIGNIFICANT_ATTRIBUTES: set[str] = {
33  ATTR_WEATHER_APPARENT_TEMPERATURE,
34  ATTR_WEATHER_CLOUD_COVERAGE,
35  ATTR_WEATHER_DEW_POINT,
36  ATTR_WEATHER_HUMIDITY,
37  ATTR_WEATHER_OZONE,
38  ATTR_WEATHER_PRESSURE,
39  ATTR_WEATHER_TEMPERATURE,
40  ATTR_WEATHER_UV_INDEX,
41  ATTR_WEATHER_VISIBILITY,
42  ATTR_WEATHER_WIND_BEARING,
43  ATTR_WEATHER_WIND_GUST_SPEED,
44  ATTR_WEATHER_WIND_SPEED,
45 }
46 
47 VALID_CARDINAL_DIRECTIONS: list[str] = [
48  "n",
49  "nne",
50  "ne",
51  "ene",
52  "e",
53  "ese",
54  "se",
55  "sse",
56  "s",
57  "ssw",
58  "sw",
59  "wsw",
60  "w",
61  "wnw",
62  "nw",
63  "nnw",
64 ]
65 
66 
67 def _cardinal_to_degrees(value: str | float | None) -> int | float | None:
68  """Translate a cardinal direction into azimuth angle (degrees)."""
69  if not isinstance(value, str):
70  return value
71 
72  try:
73  return float(360 / 16 * VALID_CARDINAL_DIRECTIONS.index(value.lower()))
74  except ValueError:
75  return None
76 
77 
78 @callback
80  hass: HomeAssistant,
81  old_state: str,
82  old_attrs: dict,
83  new_state: str,
84  new_attrs: dict,
85  **kwargs: Any,
86 ) -> bool | None:
87  """Test if state significantly changed."""
88  # state changes are always significant
89  if old_state != new_state:
90  return True
91 
92  old_attrs_s = set(
93  {k: v for k, v in old_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items()
94  )
95  new_attrs_s = set(
96  {k: v for k, v in new_attrs.items() if k in SIGNIFICANT_ATTRIBUTES}.items()
97  )
98  changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s}
99 
100  for attr_name in changed_attrs:
101  old_attr_value = old_attrs.get(attr_name)
102  new_attr_value = new_attrs.get(attr_name)
103  absolute_change: float | None = None
104  if attr_name == ATTR_WEATHER_WIND_BEARING:
105  old_attr_value = _cardinal_to_degrees(old_attr_value)
106  new_attr_value = _cardinal_to_degrees(new_attr_value)
107 
108  if new_attr_value is None or not check_valid_float(new_attr_value):
109  # New attribute value is invalid, ignore it
110  continue
111 
112  if old_attr_value is None or not check_valid_float(old_attr_value):
113  # Old attribute value was invalid, we should report again
114  return True
115 
116  if attr_name in (
117  ATTR_WEATHER_APPARENT_TEMPERATURE,
118  ATTR_WEATHER_DEW_POINT,
119  ATTR_WEATHER_TEMPERATURE,
120  ):
121  if (
122  unit := new_attrs.get(ATTR_WEATHER_TEMPERATURE_UNIT)
123  ) is not None and unit == UnitOfTemperature.FAHRENHEIT:
124  absolute_change = 1.0
125  else:
126  absolute_change = 0.5
127 
128  if attr_name in (
129  ATTR_WEATHER_WIND_GUST_SPEED,
130  ATTR_WEATHER_WIND_SPEED,
131  ):
132  if (
133  unit := new_attrs.get(ATTR_WEATHER_WIND_SPEED_UNIT)
134  ) is None or unit in (
135  UnitOfSpeed.KILOMETERS_PER_HOUR,
136  UnitOfSpeed.MILES_PER_HOUR, # 1km/h = 0.62mi/s
137  UnitOfSpeed.FEET_PER_SECOND, # 1km/h = 0.91ft/s
138  ):
139  absolute_change = 1.0
140  elif unit == UnitOfSpeed.METERS_PER_SECOND: # 1km/h = 0.277m/s
141  absolute_change = 0.5
142 
143  if attr_name in (
144  ATTR_WEATHER_CLOUD_COVERAGE, # range 0-100%
145  ATTR_WEATHER_HUMIDITY, # range 0-100%
146  ATTR_WEATHER_OZONE, # range ~20-100ppm
147  ATTR_WEATHER_VISIBILITY, # range 0-240km (150mi)
148  ATTR_WEATHER_WIND_BEARING, # range 0-359°
149  ):
150  absolute_change = 1.0
151 
152  if attr_name == ATTR_WEATHER_UV_INDEX: # range 1-11
153  absolute_change = 0.1
154 
155  if attr_name == ATTR_WEATHER_PRESSURE: # local variation of around 100 hpa
156  if (unit := new_attrs.get(ATTR_WEATHER_PRESSURE_UNIT)) is None or unit in (
157  UnitOfPressure.HPA,
158  UnitOfPressure.MBAR, # 1hPa = 1mbar
159  UnitOfPressure.MMHG, # 1hPa = 0.75mmHg
160  ):
161  absolute_change = 1.0
162  elif unit == UnitOfPressure.INHG: # 1hPa = 0.03inHg
163  absolute_change = 0.05
164 
165  # check for significant attribute value change
166  if absolute_change is not None:
167  if check_absolute_change(old_attr_value, new_attr_value, absolute_change):
168  return True
169 
170  # no significant attribute change detected
171  return False
bool|None async_check_significant_change(HomeAssistant hass, str old_state, dict old_attrs, str new_state, dict new_attrs, **Any kwargs)
int|float|None _cardinal_to_degrees(str|float|None value)
bool check_absolute_change(float|None val1, float|None val2, float change)