1
mirror of https://github.com/home-assistant/core synced 2024-10-01 05:30:36 +02:00
ha-core/homeassistant/helpers/significant_change.py

207 lines
6.0 KiB
Python
Raw Normal View History

2021-01-26 14:13:27 +01:00
"""Helpers to help find if an entity has changed significantly.
Does this with help of the integration. Looks at significant_change.py
platform for a function `async_check_significant_change`:
```python
from typing import Optional
from homeassistant.core import HomeAssistant
async def async_check_significant_change(
hass: HomeAssistant,
old_state: str,
old_attrs: dict,
new_state: str,
new_attrs: dict,
**kwargs,
) -> Optional[bool]
```
Return boolean to indicate if significantly changed. If don't know, return None.
**kwargs will allow us to expand this feature in the future, like passing in a
level of significance.
The following cases will never be passed to your function:
- if either state is unknown/unavailable
- state adding/removing
"""
from __future__ import annotations
2021-01-26 14:13:27 +01:00
from types import MappingProxyType
2021-03-17 18:34:19 +01:00
from typing import Any, Callable, Optional, Union
2021-01-26 14:13:27 +01:00
from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, State, callback
from .integration_platform import async_process_integration_platforms
PLATFORM = "significant_change"
DATA_FUNCTIONS = "significant_change"
CheckTypeFunc = Callable[
[
HomeAssistant,
str,
Union[dict, MappingProxyType],
str,
Union[dict, MappingProxyType],
],
Optional[bool],
]
ExtraCheckTypeFunc = Callable[
[
HomeAssistant,
str,
Union[dict, MappingProxyType],
Any,
str,
Union[dict, MappingProxyType],
Any,
],
Optional[bool],
]
2021-01-26 14:13:27 +01:00
async def create_checker(
hass: HomeAssistant,
_domain: str,
2021-03-17 18:34:19 +01:00
extra_significant_check: ExtraCheckTypeFunc | None = None,
) -> SignificantlyChangedChecker:
2021-01-26 14:13:27 +01:00
"""Create a significantly changed checker for a domain."""
await _initialize(hass)
return SignificantlyChangedChecker(hass, extra_significant_check)
2021-01-26 14:13:27 +01:00
# Marked as singleton so multiple calls all wait for same output.
async def _initialize(hass: HomeAssistant) -> None:
"""Initialize the functions."""
if DATA_FUNCTIONS in hass.data:
return
functions = hass.data[DATA_FUNCTIONS] = {}
async def process_platform(
hass: HomeAssistant, component_name: str, platform: Any
) -> None:
"""Process a significant change platform."""
functions[component_name] = platform.async_check_significant_change
await async_process_integration_platforms(hass, PLATFORM, process_platform)
2021-03-17 18:34:19 +01:00
def either_one_none(val1: Any | None, val2: Any | None) -> bool:
"""Test if exactly one value is None."""
return (val1 is None and val2 is not None) or (val1 is not None and val2 is None)
def check_numeric_changed(
2021-03-17 18:34:19 +01:00
val1: int | float | None,
val2: int | float | None,
change: int | float,
) -> bool:
"""Check if two numeric values have changed."""
if val1 is None and val2 is None:
return False
if either_one_none(val1, val2):
return True
assert val1 is not None
assert val2 is not None
if abs(val1 - val2) >= change:
return True
return False
2021-01-26 14:13:27 +01:00
class SignificantlyChangedChecker:
"""Class to keep track of entities to see if they have significantly changed.
Will always compare the entity to the last entity that was considered significant.
"""
def __init__(
self,
hass: HomeAssistant,
2021-03-17 18:34:19 +01:00
extra_significant_check: ExtraCheckTypeFunc | None = None,
) -> None:
2021-01-26 14:13:27 +01:00
"""Test if an entity has significantly changed."""
self.hass = hass
2021-03-17 18:34:19 +01:00
self.last_approved_entities: dict[str, tuple[State, Any]] = {}
self.extra_significant_check = extra_significant_check
2021-01-26 14:13:27 +01:00
@callback
def async_is_significant_change(
2021-03-17 18:34:19 +01:00
self, new_state: State, *, extra_arg: Any | None = None
) -> bool:
"""Return if this was a significant change.
Extra kwargs are passed to the extra significant checker.
"""
2021-03-17 18:34:19 +01:00
old_data: tuple[State, Any] | None = self.last_approved_entities.get(
2021-01-26 14:13:27 +01:00
new_state.entity_id
)
# First state change is always ok to report
if old_data is None:
self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
2021-01-26 14:13:27 +01:00
return True
old_state, old_extra_arg = old_data
2021-01-26 14:13:27 +01:00
# Handle state unknown or unavailable
if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
if new_state.state == old_state.state:
return False
self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
2021-01-26 14:13:27 +01:00
return True
# If last state was unknown/unavailable, also significant.
if old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
self.last_approved_entities[new_state.entity_id] = (new_state, extra_arg)
2021-01-26 14:13:27 +01:00
return True
2021-03-17 18:34:19 +01:00
functions: dict[str, CheckTypeFunc] | None = self.hass.data.get(DATA_FUNCTIONS)
2021-01-26 14:13:27 +01:00
if functions is None:
raise RuntimeError("Significant Change not initialized")
check_significantly_changed = functions.get(new_state.domain)
if check_significantly_changed is not None:
result = check_significantly_changed(
self.hass,
old_state.state,
old_state.attributes,
new_state.state,
new_state.attributes,
)
2021-01-26 14:13:27 +01:00
if result is False:
return False
2021-01-26 14:13:27 +01:00
if self.extra_significant_check is not None:
result = self.extra_significant_check(
self.hass,
old_state.state,
old_state.attributes,
old_extra_arg,
new_state.state,
new_state.attributes,
extra_arg,
)
if result is False:
return False
2021-01-26 14:13:27 +01:00
# Result is either True or None.
# None means the function doesn't know. For now assume it's True
self.last_approved_entities[new_state.entity_id] = (
new_state,
extra_arg,
)
2021-01-26 14:13:27 +01:00
return True