From 352d0870e3ae85d8107494d038f5b1dc7996b41c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 26 Jan 2021 22:11:06 +0100 Subject: [PATCH] Light significant changes + sensor tweaks (#45583) --- .../components/light/significant_change.py | 71 +++++++++++++++++++ .../components/sensor/significant_change.py | 10 +-- homeassistant/helpers/significant_change.py | 26 +++++++ .../tests/test_significant_change.py | 9 +-- .../light/test_significant_change.py | 70 ++++++++++++++++++ .../sensor/test_significant_change.py | 8 +-- 6 files changed, 179 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/light/significant_change.py create mode 100644 tests/components/light/test_significant_change.py diff --git a/homeassistant/components/light/significant_change.py b/homeassistant/components/light/significant_change.py new file mode 100644 index 00000000000..a0bd5203101 --- /dev/null +++ b/homeassistant/components/light/significant_change.py @@ -0,0 +1,71 @@ +"""Helper to test significant Light state changes.""" +from typing import Any, Optional + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.significant_change import ( + check_numeric_changed, + either_one_none, +) + +from . import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, +) + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + if old_state != new_state: + return True + + if old_attrs.get(ATTR_EFFECT) != new_attrs.get(ATTR_EFFECT): + return True + + 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): + return True + + # Range 0..100 + if check_numeric_changed(old_color[1], new_color[1], 3): + return True + + if check_numeric_changed( + old_attrs.get(ATTR_BRIGHTNESS), new_attrs.get(ATTR_BRIGHTNESS), 3 + ): + return True + + if check_numeric_changed( + # Default range 153..500 + old_attrs.get(ATTR_COLOR_TEMP), + new_attrs.get(ATTR_COLOR_TEMP), + 5, + ): + return True + + if check_numeric_changed( + # Range 0..255 + old_attrs.get(ATTR_WHITE_VALUE), + new_attrs.get(ATTR_WHITE_VALUE), + 5, + ): + return True + + return False diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py index 74e2779c8d7..2c281c0a046 100644 --- a/homeassistant/components/sensor/significant_change.py +++ b/homeassistant/components/sensor/significant_change.py @@ -1,5 +1,5 @@ """Helper to test significant sensor state changes.""" -from typing import Any, Optional +from typing import Any, Optional, Union from homeassistant.const import ( ATTR_DEVICE_CLASS, @@ -28,18 +28,18 @@ def async_check_significant_change( if device_class == DEVICE_CLASS_TEMPERATURE: if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: - change = 0.03 + change: Union[float, int] = 1 else: - change = 0.05 + change = 0.5 old_value = float(old_state) new_value = float(new_state) - return abs(1 - old_value / new_value) > change + return abs(old_value - new_value) >= change if device_class in (DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY): old_value = float(old_state) new_value = float(new_state) - return abs(old_value - new_value) > 2 + return abs(old_value - new_value) >= 1 return None diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py index b600e97c4e9..0a5b6aae10d 100644 --- a/homeassistant/helpers/significant_change.py +++ b/homeassistant/helpers/significant_change.py @@ -73,6 +73,32 @@ async def _initialize(hass: HomeAssistant) -> None: await async_process_integration_platforms(hass, PLATFORM, process_platform) +def either_one_none(val1: Optional[Any], val2: Optional[Any]) -> bool: + """Test if exactly one value is None.""" + return (val1 is None and val2 is not None) or (val1 is not None and val2 is None) + + +def check_numeric_changed( + val1: Optional[Union[int, float]], + val2: Optional[Union[int, float]], + change: Union[int, float], +) -> bool: + """Check if two numeric values have changed.""" + if val1 is None and val2 is None: + return False + + if either_one_none(val1, val2): + return True + + assert val1 is not None + assert val2 is not None + + if abs(val1 - val2) >= change: + return True + + return False + + class SignificantlyChangedChecker: """Class to keep track of entities to see if they have significantly changed. diff --git a/script/scaffold/templates/significant_change/tests/test_significant_change.py b/script/scaffold/templates/significant_change/tests/test_significant_change.py index b377211983c..ac339071748 100644 --- a/script/scaffold/templates/significant_change/tests/test_significant_change.py +++ b/script/scaffold/templates/significant_change/tests/test_significant_change.py @@ -1,14 +1,11 @@ -"""Test the sensor significant change platform.""" +"""Test the NEW_NAME significant change platform.""" from homeassistant.components.NEW_DOMAIN.significant_change import ( async_check_significant_change, ) -from homeassistant.const import ATTR_DEVICE_CLASS async def test_significant_change(): - """Detect NEW_NAME significant change.""" - attrs = {ATTR_DEVICE_CLASS: "some_device_class"} - + """Detect NEW_NAME significant changes.""" + attrs = {} assert not async_check_significant_change(None, "on", attrs, "on", attrs) - assert async_check_significant_change(None, "on", attrs, "off", attrs) diff --git a/tests/components/light/test_significant_change.py b/tests/components/light/test_significant_change.py new file mode 100644 index 00000000000..f935ec8df1a --- /dev/null +++ b/tests/components/light/test_significant_change.py @@ -0,0 +1,70 @@ +"""Test the Light significant change platform.""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, +) +from homeassistant.components.light.significant_change import ( + async_check_significant_change, +) + + +async def test_significant_change(): + """Detect Light significant changes.""" + assert not async_check_significant_change(None, "on", {}, "on", {}) + assert async_check_significant_change(None, "on", {}, "off", {}) + + # Brightness + assert not async_check_significant_change( + None, "on", {ATTR_BRIGHTNESS: 60}, "on", {ATTR_BRIGHTNESS: 61} + ) + assert async_check_significant_change( + None, "on", {ATTR_BRIGHTNESS: 60}, "on", {ATTR_BRIGHTNESS: 63} + ) + + # Color temp + assert not async_check_significant_change( + None, "on", {ATTR_COLOR_TEMP: 60}, "on", {ATTR_COLOR_TEMP: 64} + ) + assert async_check_significant_change( + None, "on", {ATTR_COLOR_TEMP: 60}, "on", {ATTR_COLOR_TEMP: 65} + ) + + # White value + assert not async_check_significant_change( + None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 64} + ) + assert async_check_significant_change( + None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 65} + ) + + # Effect + for eff1, eff2, expected in ( + (None, None, False), + (None, "colorloop", True), + ("colorloop", None, True), + ("colorloop", "jump", True), + ("colorloop", "colorloop", False), + ): + result = async_check_significant_change( + None, "on", {ATTR_EFFECT: eff1}, "on", {ATTR_EFFECT: eff2} + ) + assert result is expected + + # Hue + assert not async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [124, 20]} + ) + assert async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [125, 20]} + ) + + # Satursation + assert not async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [120, 22]} + ) + assert async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [120, 23]} + ) diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py index 1013d96f524..12b74345011 100644 --- a/tests/components/sensor/test_significant_change.py +++ b/tests/components/sensor/test_significant_change.py @@ -34,10 +34,10 @@ async def test_significant_change_temperature(): ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, } assert async_check_significant_change( - None, "70", freedom_attrs, "74", freedom_attrs + None, "70", freedom_attrs, "71", freedom_attrs ) assert not async_check_significant_change( - None, "70", freedom_attrs, "71", freedom_attrs + None, "70", freedom_attrs, "70.5", freedom_attrs ) @@ -47,7 +47,7 @@ async def test_significant_change_battery(): 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, "97", attrs) + assert async_check_significant_change(None, "100", attrs, "99", attrs) async def test_significant_change_humidity(): @@ -56,4 +56,4 @@ async def test_significant_change_humidity(): 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, "97", attrs) + assert async_check_significant_change(None, "100", attrs, "99", attrs)