Add significant change support to AQI type sensors (#55833)

This commit is contained in:
Erik Montnemery 2021-09-08 21:47:48 +02:00 committed by GitHub
parent bb6c2093a2
commit 232943c93d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 152 additions and 83 deletions

View File

@ -2311,16 +2311,12 @@ class SensorStateTrait(_Trait):
name = TRAIT_SENSOR_STATE name = TRAIT_SENSOR_STATE
commands = [] commands = []
@staticmethod @classmethod
def supported(domain, features, device_class, _): def supported(cls, domain, features, device_class, _):
"""Test if state is supported.""" """Test if state is supported."""
return domain == sensor.DOMAIN and device_class in ( return (
sensor.DEVICE_CLASS_AQI, domain == sensor.DOMAIN
sensor.DEVICE_CLASS_CO, and device_class in SensorStateTrait.sensor_types.keys()
sensor.DEVICE_CLASS_CO2,
sensor.DEVICE_CLASS_PM25,
sensor.DEVICE_CLASS_PM10,
sensor.DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS,
) )
def sync_attributes(self): def sync_attributes(self):

View File

@ -4,10 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.significant_change import ( from homeassistant.helpers.significant_change import check_absolute_change
check_numeric_changed,
either_one_none,
)
from . import ( from . import (
ATTR_BRIGHTNESS, ATTR_BRIGHTNESS,
@ -37,24 +34,21 @@ def async_check_significant_change(
old_color = old_attrs.get(ATTR_HS_COLOR) old_color = old_attrs.get(ATTR_HS_COLOR)
new_color = new_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: if old_color and new_color:
# Range 0..360 # 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 return True
# Range 0..100 # 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 return True
if check_numeric_changed( if check_absolute_change(
old_attrs.get(ATTR_BRIGHTNESS), new_attrs.get(ATTR_BRIGHTNESS), 3 old_attrs.get(ATTR_BRIGHTNESS), new_attrs.get(ATTR_BRIGHTNESS), 3
): ):
return True return True
if check_numeric_changed( if check_absolute_change(
# Default range 153..500 # Default range 153..500
old_attrs.get(ATTR_COLOR_TEMP), old_attrs.get(ATTR_COLOR_TEMP),
new_attrs.get(ATTR_COLOR_TEMP), new_attrs.get(ATTR_COLOR_TEMP),
@ -62,7 +56,7 @@ def async_check_significant_change(
): ):
return True return True
if check_numeric_changed( if check_absolute_change(
# Range 0..255 # Range 0..255
old_attrs.get(ATTR_WHITE_VALUE), old_attrs.get(ATTR_WHITE_VALUE),
new_attrs.get(ATTR_WHITE_VALUE), new_attrs.get(ATTR_WHITE_VALUE),

View File

@ -9,8 +9,33 @@ from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
from homeassistant.core import HomeAssistant, callback 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 @callback
@ -28,20 +53,35 @@ def async_check_significant_change(
if device_class is None: if device_class is None:
return None return None
absolute_change: float | None = None
percentage_change: float | None = None
if device_class == DEVICE_CLASS_TEMPERATURE: if device_class == DEVICE_CLASS_TEMPERATURE:
if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT:
change: float | int = 1 absolute_change = 1.0
else: else:
change = 0.5 absolute_change = 0.5
old_value = float(old_state)
new_value = float(new_state)
return abs(old_value - new_value) >= change
if device_class in (DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY): if device_class in (DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY):
old_value = float(old_state) absolute_change = 1.0
new_value = float(new_state)
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 return None

View File

@ -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) 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, val1: int | float | None,
val2: int | float | None, val2: int | float | None,
change: int | float, change: int | float,
) -> bool: ) -> bool:
"""Check if two numeric values have changed.""" """Check if two numeric values have changed."""
if val1 is None and val2 is None: return _check_numeric_change(
return False val1, val2, change, lambda val1, val2: abs(val1 - val2)
)
if either_one_none(val1, val2):
return True
assert val1 is not None def check_percentage_change(
assert val2 is not None 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: def percentage_change(old_state: int | float, new_state: int | float) -> float:
return True 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: class SignificantlyChangedChecker:

View File

@ -1,5 +1,8 @@
"""Test the sensor significant change platform.""" """Test the sensor significant change platform."""
import pytest
from homeassistant.components.sensor.significant_change import ( from homeassistant.components.sensor.significant_change import (
DEVICE_CLASS_AQI,
DEVICE_CLASS_BATTERY, DEVICE_CLASS_BATTERY,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
@ -12,48 +15,54 @@ from homeassistant.const import (
TEMP_FAHRENHEIT, TEMP_FAHRENHEIT,
) )
AQI_ATTRS = {
ATTR_DEVICE_CLASS: DEVICE_CLASS_AQI,
}
async def test_significant_change_temperature(): BATTERY_ATTRS = {
"""Detect temperature significant changes.""" ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY,
celsius_attrs = { }
HUMIDITY_ATTRS = {
ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY,
}
TEMP_CELSIUS_ATTRS = {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, 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, "12", celsius_attrs, "13", celsius_attrs
)
assert not async_check_significant_change(
None, "12.1", celsius_attrs, "12.2", celsius_attrs
)
freedom_attrs = { TEMP_FREEDOM_ATTRS = {
ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT,
} }
assert async_check_significant_change(
None, "70", freedom_attrs, "71", freedom_attrs
@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),
],
) )
assert not async_check_significant_change( async def test_significant_change_temperature(old_state, new_state, attrs, result):
None, "70", freedom_attrs, "70.5", freedom_attrs """Detect temperature significant changes."""
assert (
async_check_significant_change(None, old_state, attrs, new_state, attrs)
is result
) )
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)