mirror of https://github.com/home-assistant/core
Add significant change support to AQI type sensors (#55833)
This commit is contained in:
parent
bb6c2093a2
commit
232943c93d
|
@ -2311,16 +2311,12 @@ class SensorStateTrait(_Trait):
|
|||
name = TRAIT_SENSOR_STATE
|
||||
commands = []
|
||||
|
||||
@staticmethod
|
||||
def supported(domain, features, device_class, _):
|
||||
@classmethod
|
||||
def supported(cls, domain, features, device_class, _):
|
||||
"""Test if state is supported."""
|
||||
return domain == sensor.DOMAIN and device_class in (
|
||||
sensor.DEVICE_CLASS_AQI,
|
||||
sensor.DEVICE_CLASS_CO,
|
||||
sensor.DEVICE_CLASS_CO2,
|
||||
sensor.DEVICE_CLASS_PM25,
|
||||
sensor.DEVICE_CLASS_PM10,
|
||||
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
return (
|
||||
domain == sensor.DOMAIN
|
||||
and device_class in SensorStateTrait.sensor_types.keys()
|
||||
)
|
||||
|
||||
def sync_attributes(self):
|
||||
|
|
|
@ -4,10 +4,7 @@ from __future__ import annotations
|
|||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.significant_change import (
|
||||
check_numeric_changed,
|
||||
either_one_none,
|
||||
)
|
||||
from homeassistant.helpers.significant_change import check_absolute_change
|
||||
|
||||
from . import (
|
||||
ATTR_BRIGHTNESS,
|
||||
|
@ -37,24 +34,21 @@ def async_check_significant_change(
|
|||
old_color = old_attrs.get(ATTR_HS_COLOR)
|
||||
new_color = new_attrs.get(ATTR_HS_COLOR)
|
||||
|
||||
if either_one_none(old_color, new_color):
|
||||
return True
|
||||
|
||||
if old_color and new_color:
|
||||
# Range 0..360
|
||||
if check_numeric_changed(old_color[0], new_color[0], 5):
|
||||
if check_absolute_change(old_color[0], new_color[0], 5):
|
||||
return True
|
||||
|
||||
# Range 0..100
|
||||
if check_numeric_changed(old_color[1], new_color[1], 3):
|
||||
if check_absolute_change(old_color[1], new_color[1], 3):
|
||||
return True
|
||||
|
||||
if check_numeric_changed(
|
||||
if check_absolute_change(
|
||||
old_attrs.get(ATTR_BRIGHTNESS), new_attrs.get(ATTR_BRIGHTNESS), 3
|
||||
):
|
||||
return True
|
||||
|
||||
if check_numeric_changed(
|
||||
if check_absolute_change(
|
||||
# Default range 153..500
|
||||
old_attrs.get(ATTR_COLOR_TEMP),
|
||||
new_attrs.get(ATTR_COLOR_TEMP),
|
||||
|
@ -62,7 +56,7 @@ def async_check_significant_change(
|
|||
):
|
||||
return True
|
||||
|
||||
if check_numeric_changed(
|
||||
if check_absolute_change(
|
||||
# Range 0..255
|
||||
old_attrs.get(ATTR_WHITE_VALUE),
|
||||
new_attrs.get(ATTR_WHITE_VALUE),
|
||||
|
|
|
@ -9,8 +9,33 @@ from homeassistant.const import (
|
|||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.significant_change import (
|
||||
check_absolute_change,
|
||||
check_percentage_change,
|
||||
)
|
||||
|
||||
from . import DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE
|
||||
from . import (
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_CO,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
)
|
||||
|
||||
|
||||
def _absolute_and_relative_change(
|
||||
old_state: int | float | None,
|
||||
new_state: int | float | None,
|
||||
absolute_change: int | float,
|
||||
percentage_change: int | float,
|
||||
) -> bool:
|
||||
return check_absolute_change(
|
||||
old_state, new_state, absolute_change
|
||||
) and check_percentage_change(old_state, new_state, percentage_change)
|
||||
|
||||
|
||||
@callback
|
||||
|
@ -28,20 +53,35 @@ def async_check_significant_change(
|
|||
if device_class is None:
|
||||
return None
|
||||
|
||||
absolute_change: float | None = None
|
||||
percentage_change: float | None = None
|
||||
if device_class == DEVICE_CLASS_TEMPERATURE:
|
||||
if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT:
|
||||
change: float | int = 1
|
||||
absolute_change = 1.0
|
||||
else:
|
||||
change = 0.5
|
||||
|
||||
old_value = float(old_state)
|
||||
new_value = float(new_state)
|
||||
return abs(old_value - new_value) >= change
|
||||
absolute_change = 0.5
|
||||
|
||||
if device_class in (DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY):
|
||||
old_value = float(old_state)
|
||||
new_value = float(new_state)
|
||||
absolute_change = 1.0
|
||||
|
||||
return abs(old_value - new_value) >= 1
|
||||
if device_class in (
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_CO,
|
||||
DEVICE_CLASS_CO2,
|
||||
DEVICE_CLASS_PM25,
|
||||
DEVICE_CLASS_PM10,
|
||||
DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
|
||||
):
|
||||
absolute_change = 1.0
|
||||
percentage_change = 2.0
|
||||
|
||||
if absolute_change is not None and percentage_change is not None:
|
||||
return _absolute_and_relative_change(
|
||||
float(old_state), float(new_state), absolute_change, percentage_change
|
||||
)
|
||||
if absolute_change is not None:
|
||||
return check_absolute_change(
|
||||
float(old_state), float(new_state), absolute_change
|
||||
)
|
||||
|
||||
return None
|
||||
|
|
|
@ -95,25 +95,55 @@ def either_one_none(val1: Any | None, val2: Any | None) -> bool:
|
|||
return (val1 is None and val2 is not None) or (val1 is not None and val2 is None)
|
||||
|
||||
|
||||
def check_numeric_changed(
|
||||
def _check_numeric_change(
|
||||
old_state: int | float | None,
|
||||
new_state: int | float | None,
|
||||
change: int | float,
|
||||
metric: Callable[[int | float, int | float], int | float],
|
||||
) -> bool:
|
||||
"""Check if two numeric values have changed."""
|
||||
if old_state is None and new_state is None:
|
||||
return False
|
||||
|
||||
if either_one_none(old_state, new_state):
|
||||
return True
|
||||
|
||||
assert old_state is not None
|
||||
assert new_state is not None
|
||||
|
||||
if metric(old_state, new_state) >= change:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def check_absolute_change(
|
||||
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
|
||||
return _check_numeric_change(
|
||||
val1, val2, change, lambda val1, val2: abs(val1 - val2)
|
||||
)
|
||||
|
||||
if either_one_none(val1, val2):
|
||||
return True
|
||||
|
||||
assert val1 is not None
|
||||
assert val2 is not None
|
||||
def check_percentage_change(
|
||||
old_state: int | float | None,
|
||||
new_state: int | float | None,
|
||||
change: int | float,
|
||||
) -> bool:
|
||||
"""Check if two numeric values have changed."""
|
||||
|
||||
if abs(val1 - val2) >= change:
|
||||
return True
|
||||
def percentage_change(old_state: int | float, new_state: int | float) -> float:
|
||||
if old_state == new_state:
|
||||
return 0
|
||||
try:
|
||||
return (abs(new_state - old_state) / old_state) * 100.0
|
||||
except ZeroDivisionError:
|
||||
return float("inf")
|
||||
|
||||
return False
|
||||
return _check_numeric_change(old_state, new_state, change, percentage_change)
|
||||
|
||||
|
||||
class SignificantlyChangedChecker:
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
"""Test the sensor significant change platform."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor.significant_change import (
|
||||
DEVICE_CLASS_AQI,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
|
@ -12,48 +15,54 @@ from homeassistant.const import (
|
|||
TEMP_FAHRENHEIT,
|
||||
)
|
||||
|
||||
AQI_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_AQI,
|
||||
}
|
||||
|
||||
async def test_significant_change_temperature():
|
||||
BATTERY_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY,
|
||||
}
|
||||
|
||||
HUMIDITY_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
}
|
||||
|
||||
TEMP_CELSIUS_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||
}
|
||||
|
||||
TEMP_FREEDOM_ATTRS = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"old_state,new_state,attrs,result",
|
||||
[
|
||||
("0", "1", AQI_ATTRS, True),
|
||||
("1", "0", AQI_ATTRS, True),
|
||||
("0.1", "0.5", AQI_ATTRS, False),
|
||||
("0.5", "0.1", AQI_ATTRS, False),
|
||||
("99", "100", AQI_ATTRS, False),
|
||||
("100", "99", AQI_ATTRS, False),
|
||||
("101", "99", AQI_ATTRS, False),
|
||||
("99", "101", AQI_ATTRS, True),
|
||||
("100", "100", BATTERY_ATTRS, False),
|
||||
("100", "99", BATTERY_ATTRS, True),
|
||||
("100", "100", HUMIDITY_ATTRS, False),
|
||||
("100", "99", HUMIDITY_ATTRS, True),
|
||||
("12", "12", TEMP_CELSIUS_ATTRS, False),
|
||||
("12", "13", TEMP_CELSIUS_ATTRS, True),
|
||||
("12.1", "12.2", TEMP_CELSIUS_ATTRS, False),
|
||||
("70", "71", TEMP_FREEDOM_ATTRS, True),
|
||||
("70", "70.5", TEMP_FREEDOM_ATTRS, False),
|
||||
],
|
||||
)
|
||||
async def test_significant_change_temperature(old_state, new_state, attrs, result):
|
||||
"""Detect temperature significant changes."""
|
||||
celsius_attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS,
|
||||
}
|
||||
assert not async_check_significant_change(
|
||||
None, "12", celsius_attrs, "12", celsius_attrs
|
||||
assert (
|
||||
async_check_significant_change(None, old_state, attrs, new_state, attrs)
|
||||
is result
|
||||
)
|
||||
assert async_check_significant_change(
|
||||
None, "12", celsius_attrs, "13", celsius_attrs
|
||||
)
|
||||
assert not async_check_significant_change(
|
||||
None, "12.1", celsius_attrs, "12.2", celsius_attrs
|
||||
)
|
||||
|
||||
freedom_attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
|
||||
ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT,
|
||||
}
|
||||
assert async_check_significant_change(
|
||||
None, "70", freedom_attrs, "71", freedom_attrs
|
||||
)
|
||||
assert not async_check_significant_change(
|
||||
None, "70", freedom_attrs, "70.5", freedom_attrs
|
||||
)
|
||||
|
||||
|
||||
async def test_significant_change_battery():
|
||||
"""Detect battery significant changes."""
|
||||
attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY,
|
||||
}
|
||||
assert not async_check_significant_change(None, "100", attrs, "100", attrs)
|
||||
assert async_check_significant_change(None, "100", attrs, "99", attrs)
|
||||
|
||||
|
||||
async def test_significant_change_humidity():
|
||||
"""Detect humidity significant changes."""
|
||||
attrs = {
|
||||
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
|
||||
}
|
||||
assert not async_check_significant_change(None, "100", attrs, "100", attrs)
|
||||
assert async_check_significant_change(None, "100", attrs, "99", attrs)
|
||||
|
|
Loading…
Reference in New Issue