From 117ff21c484db51b80f107940a638fb329cdb941 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:54:08 +0100 Subject: [PATCH] Add significant Change support for number (#105863) --- homeassistant/components/number/const.py | 2 +- .../components/number/significant_change.py | 92 ++++++++++++++++++ .../number/test_significant_change.py | 94 +++++++++++++++++++ 3 files changed, 187 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/number/significant_change.py create mode 100644 tests/components/number/test_significant_change.py diff --git a/homeassistant/components/number/const.py b/homeassistant/components/number/const.py index 4107509e01f3..55d22c866489 100644 --- a/homeassistant/components/number/const.py +++ b/homeassistant/components/number/const.py @@ -78,7 +78,7 @@ __dir__ = partial(dir_with_deprecated_constants, module_globals=globals()) class NumberDeviceClass(StrEnum): """Device class for numbers.""" - # NumberDeviceClass should be aligned with NumberDeviceClass + # NumberDeviceClass should be aligned with SensorDeviceClass APPARENT_POWER = "apparent_power" """Apparent power. diff --git a/homeassistant/components/number/significant_change.py b/homeassistant/components/number/significant_change.py new file mode 100644 index 000000000000..11bca6457f1e --- /dev/null +++ b/homeassistant/components/number/significant_change.py @@ -0,0 +1,92 @@ +"""Helper to test significant Number state changes.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + UnitOfTemperature, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.significant_change import ( + check_absolute_change, + check_percentage_change, + check_valid_float, +) + +from .const import NumberDeviceClass + + +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 +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> bool | None: + """Test if state significantly changed.""" + if (device_class := new_attrs.get(ATTR_DEVICE_CLASS)) is None: + return None + + absolute_change: float | None = None + percentage_change: float | None = None + + # special for temperature + if device_class == NumberDeviceClass.TEMPERATURE: + if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfTemperature.FAHRENHEIT: + absolute_change = 1.0 + else: + absolute_change = 0.5 + + # special for percentage + elif device_class in ( + NumberDeviceClass.BATTERY, + NumberDeviceClass.HUMIDITY, + NumberDeviceClass.MOISTURE, + ): + absolute_change = 1.0 + + # special for power factor + elif device_class == NumberDeviceClass.POWER_FACTOR: + if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE: + absolute_change = 1.0 + else: + absolute_change = 0.1 + percentage_change = 2.0 + + # default for all other classified + else: + absolute_change = 1.0 + percentage_change = 2.0 + + if not check_valid_float(new_state): + # New state is invalid, don't report it + return False + + if not check_valid_float(old_state): + # Old state was invalid, we should report again + return True + + 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 + ) diff --git a/tests/components/number/test_significant_change.py b/tests/components/number/test_significant_change.py new file mode 100644 index 000000000000..1a6491f3de9f --- /dev/null +++ b/tests/components/number/test_significant_change.py @@ -0,0 +1,94 @@ +"""Test the Number significant change platform.""" +import pytest + +from homeassistant.components.number import NumberDeviceClass +from homeassistant.components.number.significant_change import ( + async_check_significant_change, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + PERCENTAGE, + UnitOfTemperature, +) + +AQI_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.AQI} +BATTERY_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.BATTERY} +CO_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.CO} +CO2_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.CO2} +HUMIDITY_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.HUMIDITY} +MOISTURE_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.MOISTURE} +PM1_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.PM1} +PM10_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.PM10} +PM25_ATTRS = {ATTR_DEVICE_CLASS: NumberDeviceClass.PM25} +POWER_FACTOR_ATTRS = { + ATTR_DEVICE_CLASS: NumberDeviceClass.POWER_FACTOR, +} +POWER_FACTOR_ATTRS_PERCENTAGE = { + ATTR_DEVICE_CLASS: NumberDeviceClass.POWER_FACTOR, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, +} +TEMP_CELSIUS_ATTRS = { + ATTR_DEVICE_CLASS: NumberDeviceClass.TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.CELSIUS, +} +TEMP_FREEDOM_ATTRS = { + ATTR_DEVICE_CLASS: NumberDeviceClass.TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: UnitOfTemperature.FAHRENHEIT, +} +VOLATILE_ORGANIC_COMPOUNDS_ATTRS = { + ATTR_DEVICE_CLASS: NumberDeviceClass.VOLATILE_ORGANIC_COMPOUNDS +} + + +@pytest.mark.parametrize( + ("old_state", "new_state", "attrs", "result"), + [ + ("0", "0.9", {}, None), + ("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), + ("0", "1", CO_ATTRS, True), + ("0.1", "0.5", CO_ATTRS, False), + ("0", "1", CO2_ATTRS, True), + ("0.1", "0.5", CO2_ATTRS, False), + ("100", "100", HUMIDITY_ATTRS, False), + ("100", "99", HUMIDITY_ATTRS, True), + ("100", "100", MOISTURE_ATTRS, False), + ("100", "99", MOISTURE_ATTRS, True), + ("0", "1", PM1_ATTRS, True), + ("0.1", "0.5", PM1_ATTRS, False), + ("0", "1", PM10_ATTRS, True), + ("0.1", "0.5", PM10_ATTRS, False), + ("0", "1", PM25_ATTRS, True), + ("0.1", "0.5", PM25_ATTRS, False), + ("0.1", "0.2", POWER_FACTOR_ATTRS, True), + ("0.1", "0.19", POWER_FACTOR_ATTRS, False), + ("1", "2", POWER_FACTOR_ATTRS_PERCENTAGE, True), + ("1", "1.9", POWER_FACTOR_ATTRS_PERCENTAGE, False), + ("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), + ("fail", "70", TEMP_FREEDOM_ATTRS, True), + ("70", "fail", TEMP_FREEDOM_ATTRS, False), + ("0", "1", VOLATILE_ORGANIC_COMPOUNDS_ATTRS, True), + ("0.1", "0.5", VOLATILE_ORGANIC_COMPOUNDS_ATTRS, False), + ], +) +async def test_significant_change_temperature( + old_state, new_state, attrs, result +) -> None: + """Detect temperature significant changes.""" + assert ( + async_check_significant_change(None, old_state, attrs, new_state, attrs) + is result + )