From a1614d6b7e603a586a3e0c1d1894683827599960 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:30:30 +0100 Subject: [PATCH] Add significant Change support for climate (#106020) --- .../components/climate/significant_change.py | 104 ++++++++++++++ .../climate/test_significant_change.py | 129 ++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 homeassistant/components/climate/significant_change.py create mode 100644 tests/components/climate/test_significant_change.py diff --git a/homeassistant/components/climate/significant_change.py b/homeassistant/components/climate/significant_change.py new file mode 100644 index 00000000000..01d3ef98558 --- /dev/null +++ b/homeassistant/components/climate/significant_change.py @@ -0,0 +1,104 @@ +"""Helper to test significant Climate state changes.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.const import UnitOfTemperature +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.significant_change import ( + check_absolute_change, + check_valid_float, +) + +from . import ( + ATTR_AUX_HEAT, + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_HUMIDITY, + ATTR_HVAC_ACTION, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, +) + +SIGNIFICANT_ATTRIBUTES: set[str] = { + ATTR_AUX_HEAT, + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_HUMIDITY, + ATTR_HVAC_ACTION, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, +} + + +@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 old_state != new_state: + return True + + old_attrs_s = set(old_attrs.items()) + new_attrs_s = set(new_attrs.items()) + changed_attrs: set[str] = {item[0] for item in old_attrs_s ^ new_attrs_s} + ha_unit = hass.config.units.temperature_unit + + for attr_name in changed_attrs: + if attr_name not in SIGNIFICANT_ATTRIBUTES: + continue + + if attr_name in [ + ATTR_AUX_HEAT, + ATTR_FAN_MODE, + ATTR_HVAC_ACTION, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, + ]: + return True + + old_attr_value = old_attrs.get(attr_name) + new_attr_value = new_attrs.get(attr_name) + if new_attr_value is None or not check_valid_float(new_attr_value): + # New attribute value is invalid, ignore it + continue + + if old_attr_value is None or not check_valid_float(old_attr_value): + # Old attribute value was invalid, we should report again + return True + + absolute_change: float | None = None + if attr_name in [ + ATTR_CURRENT_TEMPERATURE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, + ]: + if ha_unit == UnitOfTemperature.FAHRENHEIT: + absolute_change = 1.0 + else: + absolute_change = 0.5 + + if attr_name in [ATTR_CURRENT_HUMIDITY, ATTR_HUMIDITY]: + absolute_change = 1.0 + + if absolute_change and check_absolute_change( + old_attr_value, new_attr_value, absolute_change + ): + return True + + # no significant attribute change detected + return False diff --git a/tests/components/climate/test_significant_change.py b/tests/components/climate/test_significant_change.py new file mode 100644 index 00000000000..369e5e67004 --- /dev/null +++ b/tests/components/climate/test_significant_change.py @@ -0,0 +1,129 @@ +"""Test the Climate significant change platform.""" +import pytest + +from homeassistant.components.climate import ( + ATTR_AUX_HEAT, + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_FAN_MODE, + ATTR_HUMIDITY, + ATTR_HVAC_ACTION, + ATTR_PRESET_MODE, + ATTR_SWING_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + ATTR_TEMPERATURE, +) +from homeassistant.components.climate.significant_change import ( + async_check_significant_change, +) +from homeassistant.core import HomeAssistant +from homeassistant.util.unit_system import ( + METRIC_SYSTEM as METRIC, + US_CUSTOMARY_SYSTEM as IMPERIAL, + UnitSystem, +) + + +async def test_significant_state_change(hass: HomeAssistant) -> None: + """Detect Climate significant state_changes.""" + attrs = {} + assert not async_check_significant_change(hass, "on", attrs, "on", attrs) + assert async_check_significant_change(hass, "on", attrs, "off", attrs) + + +@pytest.mark.parametrize( + ("unit_system", "old_attrs", "new_attrs", "expected_result"), + [ + (METRIC, {ATTR_AUX_HEAT: "old_value"}, {ATTR_AUX_HEAT: "old_value"}, False), + (METRIC, {ATTR_AUX_HEAT: "old_value"}, {ATTR_AUX_HEAT: "new_value"}, True), + (METRIC, {ATTR_FAN_MODE: "old_value"}, {ATTR_FAN_MODE: "old_value"}, False), + (METRIC, {ATTR_FAN_MODE: "old_value"}, {ATTR_FAN_MODE: "new_value"}, True), + ( + METRIC, + {ATTR_HVAC_ACTION: "old_value"}, + {ATTR_HVAC_ACTION: "old_value"}, + False, + ), + ( + METRIC, + {ATTR_HVAC_ACTION: "old_value"}, + {ATTR_HVAC_ACTION: "new_value"}, + True, + ), + ( + METRIC, + {ATTR_PRESET_MODE: "old_value"}, + {ATTR_PRESET_MODE: "old_value"}, + False, + ), + ( + METRIC, + {ATTR_PRESET_MODE: "old_value"}, + {ATTR_PRESET_MODE: "new_value"}, + True, + ), + (METRIC, {ATTR_SWING_MODE: "old_value"}, {ATTR_SWING_MODE: "old_value"}, False), + (METRIC, {ATTR_SWING_MODE: "old_value"}, {ATTR_SWING_MODE: "new_value"}, True), + # multiple attributes + ( + METRIC, + {ATTR_HVAC_ACTION: "old_value", ATTR_PRESET_MODE: "old_value"}, + {ATTR_HVAC_ACTION: "new_value", ATTR_PRESET_MODE: "old_value"}, + True, + ), + # float attributes + (METRIC, {ATTR_CURRENT_HUMIDITY: 60.0}, {ATTR_CURRENT_HUMIDITY: 61}, True), + (METRIC, {ATTR_CURRENT_HUMIDITY: 60.0}, {ATTR_CURRENT_HUMIDITY: 60.9}, False), + ( + METRIC, + {ATTR_CURRENT_HUMIDITY: "invalid"}, + {ATTR_CURRENT_HUMIDITY: 60.0}, + True, + ), + ( + METRIC, + {ATTR_CURRENT_HUMIDITY: 60.0}, + {ATTR_CURRENT_HUMIDITY: "invalid"}, + False, + ), + ( + METRIC, + {ATTR_CURRENT_TEMPERATURE: 22.0}, + {ATTR_CURRENT_TEMPERATURE: 22.5}, + True, + ), + ( + METRIC, + {ATTR_CURRENT_TEMPERATURE: 22.0}, + {ATTR_CURRENT_TEMPERATURE: 22.4}, + False, + ), + (METRIC, {ATTR_HUMIDITY: 60.0}, {ATTR_HUMIDITY: 61.0}, True), + (METRIC, {ATTR_HUMIDITY: 60.0}, {ATTR_HUMIDITY: 60.9}, False), + (METRIC, {ATTR_TARGET_TEMP_HIGH: 31.0}, {ATTR_TARGET_TEMP_HIGH: 31.5}, True), + (METRIC, {ATTR_TARGET_TEMP_HIGH: 31.0}, {ATTR_TARGET_TEMP_HIGH: 31.4}, False), + (METRIC, {ATTR_TARGET_TEMP_LOW: 8.0}, {ATTR_TARGET_TEMP_LOW: 8.5}, True), + (METRIC, {ATTR_TARGET_TEMP_LOW: 8.0}, {ATTR_TARGET_TEMP_LOW: 8.4}, False), + (METRIC, {ATTR_TEMPERATURE: 22.0}, {ATTR_TEMPERATURE: 22.5}, True), + (METRIC, {ATTR_TEMPERATURE: 22.0}, {ATTR_TEMPERATURE: 22.4}, False), + (IMPERIAL, {ATTR_TEMPERATURE: 70.0}, {ATTR_TEMPERATURE: 71.0}, True), + (IMPERIAL, {ATTR_TEMPERATURE: 70.0}, {ATTR_TEMPERATURE: 70.9}, False), + # insignificant attributes + (METRIC, {"unknown_attr": "old_value"}, {"unknown_attr": "old_value"}, False), + (METRIC, {"unknown_attr": "old_value"}, {"unknown_attr": "new_value"}, False), + ], +) +async def test_significant_atributes_change( + hass: HomeAssistant, + unit_system: UnitSystem, + old_attrs: dict, + new_attrs: dict, + expected_result: bool, +) -> None: + """Detect Climate significant attribute changes.""" + hass.config.units = unit_system + assert ( + async_check_significant_change(hass, "state", old_attrs, "state", new_attrs) + == expected_result + )