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
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):

View File

@ -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),

View File

@ -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

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)
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:

View File

@ -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)