Home Assistant Unofficial Reference 2024.12.1
significant_change.py
Go to the documentation of this file.
1 """Helpers to help find if an entity has changed significantly.
2 
3 Does this with help of the integration. Looks at significant_change.py
4 platform for a function `async_check_significant_change`:
5 
6 ```python
7 from typing import Optional
8 from homeassistant.core import HomeAssistant
9 
10 async def async_check_significant_change(
11  hass: HomeAssistant,
12  old_state: str,
13  old_attrs: dict,
14  new_state: str,
15  new_attrs: dict,
16  **kwargs,
17 ) -> bool | None
18 ```
19 
20 Return boolean to indicate if significantly changed. If don't know, return None.
21 
22 **kwargs will allow us to expand this feature in the future, like passing in a
23 level of significance.
24 
25 The following cases will never be passed to your function:
26 - if either state is unknown/unavailable
27 - state adding/removing
28 """
29 
30 from __future__ import annotations
31 
32 from collections.abc import Callable, Mapping
33 from types import MappingProxyType
34 from typing import Any, Protocol
35 
36 from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
37 from homeassistant.core import HomeAssistant, State, callback
38 from homeassistant.util.hass_dict import HassKey
39 
40 from .integration_platform import async_process_integration_platforms
41 
42 PLATFORM = "significant_change"
43 DATA_FUNCTIONS: HassKey[dict[str, CheckTypeFunc]] = HassKey("significant_change")
44 type CheckTypeFunc = Callable[
45  [
46  HomeAssistant,
47  str,
48  dict | MappingProxyType,
49  str,
50  dict | MappingProxyType,
51  ],
52  bool | None,
53 ]
54 
55 type ExtraCheckTypeFunc = Callable[
56  [
57  HomeAssistant,
58  str,
59  dict | MappingProxyType,
60  Any,
61  str,
62  dict | MappingProxyType,
63  Any,
64  ],
65  bool | None,
66 ]
67 
68 
69 class SignificantChangeProtocol(Protocol):
70  """Define the format of significant_change platforms."""
71 
73  self,
74  hass: HomeAssistant,
75  old_state: str,
76  old_attrs: Mapping[str, Any],
77  new_state: str,
78  new_attrs: Mapping[str, Any],
79  ) -> bool | None:
80  """Test if state significantly changed."""
81 
82 
83 async def create_checker(
84  hass: HomeAssistant,
85  _domain: str,
86  extra_significant_check: ExtraCheckTypeFunc | None = None,
87 ) -> SignificantlyChangedChecker:
88  """Create a significantly changed checker for a domain."""
89  await _initialize(hass)
90  return SignificantlyChangedChecker(hass, extra_significant_check)
91 
92 
93 # Marked as singleton so multiple calls all wait for same output.
94 async def _initialize(hass: HomeAssistant) -> None:
95  """Initialize the functions."""
96  if DATA_FUNCTIONS in hass.data:
97  return
98 
99  functions = hass.data[DATA_FUNCTIONS] = {}
100 
101  @callback
102  def process_platform(
103  hass: HomeAssistant,
104  component_name: str,
105  platform: SignificantChangeProtocol,
106  ) -> None:
107  """Process a significant change platform."""
108  functions[component_name] = platform.async_check_significant_change
109 
110  await async_process_integration_platforms(hass, PLATFORM, process_platform)
111 
112 
113 def either_one_none(val1: Any | None, val2: Any | None) -> bool:
114  """Test if exactly one value is None."""
115  return (val1 is None and val2 is not None) or (val1 is not None and val2 is None)
116 
117 
119  old_state: float | None,
120  new_state: float | None,
121  change: float,
122  metric: Callable[[int | float, int | float], int | float],
123 ) -> bool:
124  """Check if two numeric values have changed."""
125  if old_state is None and new_state is None:
126  return False
127 
128  if either_one_none(old_state, new_state):
129  return True
130 
131  assert old_state is not None
132  assert new_state is not None
133 
134  if metric(old_state, new_state) >= change:
135  return True
136 
137  return False
138 
139 
141  val1: float | None,
142  val2: float | None,
143  change: float,
144 ) -> bool:
145  """Check if two numeric values have changed."""
146  return _check_numeric_change(
147  val1, val2, change, lambda val1, val2: abs(val1 - val2)
148  )
149 
150 
152  old_state: float | None,
153  new_state: float | None,
154  change: float,
155 ) -> bool:
156  """Check if two numeric values have changed."""
157 
158  def percentage_change(old_state: float, new_state: float) -> float:
159  if old_state == new_state:
160  return 0
161  try:
162  return (abs(new_state - old_state) / old_state) * 100.0
163  except ZeroDivisionError:
164  return float("inf")
165 
166  return _check_numeric_change(old_state, new_state, change, percentage_change)
167 
168 
169 def check_valid_float(value: str | float) -> bool:
170  """Check if given value is a valid float."""
171  try:
172  float(value)
173  except ValueError:
174  return False
175  return True
176 
177 
179  """Class to keep track of entities to see if they have significantly changed.
180 
181  Will always compare the entity to the last entity that was considered significant.
182  """
183 
184  def __init__(
185  self,
186  hass: HomeAssistant,
187  extra_significant_check: ExtraCheckTypeFunc | None = None,
188  ) -> None:
189  """Test if an entity has significantly changed."""
190  self.hasshass = hass
191  self.last_approved_entities: dict[str, tuple[State, Any]] = {}
192  self.extra_significant_checkextra_significant_check = extra_significant_check
193 
194  @callback
196  self, new_state: State, *, extra_arg: Any | None = None
197  ) -> bool:
198  """Return if this was a significant change.
199 
200  Extra kwargs are passed to the extra significant checker.
201  """
202  old_data: tuple[State, Any] | None = self.last_approved_entities.get(
203  new_state.entity_id
204  )
205 
206  # First state change is always ok to report
207  if old_data is None:
208  self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
209  return True
210 
211  old_state, old_extra_arg = old_data
212 
213  # Handle state unknown or unavailable
214  if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
215  if new_state.state == old_state.state:
216  return False
217 
218  self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
219  return True
220 
221  # If last state was unknown/unavailable, also significant.
222  if old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
223  self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
224  return True
225 
226  functions = self.hasshass.data.get(DATA_FUNCTIONS)
227 
228  if functions is None:
229  raise RuntimeError("Significant Change not initialized")
230 
231  check_significantly_changed = functions.get(new_state.domain)
232 
233  if check_significantly_changed is not None:
234  result = check_significantly_changed(
235  self.hasshass,
236  old_state.state,
237  old_state.attributes,
238  new_state.state,
239  new_state.attributes,
240  )
241 
242  if result is False:
243  return False
244 
245  if self.extra_significant_checkextra_significant_check is not None:
246  result = self.extra_significant_checkextra_significant_check(
247  self.hasshass,
248  old_state.state,
249  old_state.attributes,
250  old_extra_arg,
251  new_state.state,
252  new_state.attributes,
253  extra_arg,
254  )
255 
256  if result is False:
257  return False
258 
259  # Result is either True or None.
260  # None means the function doesn't know. For now assume it's True
261  self.last_approved_entities[new_state.entity_id] = (
262  new_state,
263  extra_arg,
264  )
265  return True
bool|None async_check_significant_change(self, HomeAssistant hass, str old_state, Mapping[str, Any] old_attrs, str new_state, Mapping[str, Any] new_attrs)
None __init__(self, HomeAssistant hass, ExtraCheckTypeFunc|None extra_significant_check=None)
bool async_is_significant_change(self, State new_state, *Any|None extra_arg=None)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
None async_process_integration_platforms(HomeAssistant hass, str platform_name, Callable[[HomeAssistant, str, Any], Awaitable[None]|None] process_platform, bool wait_for_platforms=False)
SignificantlyChangedChecker create_checker(HomeAssistant hass, str _domain, ExtraCheckTypeFunc|None extra_significant_check=None)
bool check_absolute_change(float|None val1, float|None val2, float change)
bool check_percentage_change(float|None old_state, float|None new_state, float change)
bool _check_numeric_change(float|None old_state, float|None new_state, float change, Callable[[int|float, int|float], int|float] metric)
bool either_one_none(Any|None val1, Any|None val2)