From 891a48e8e5b61cadd6f9aa6959dd4fd2e947fd00 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Thu, 27 Nov 2025 09:48:23 -0800 Subject: [PATCH] Add bidirectional color mode change detection (issue #1275) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extract _has_color_mode_changed() function that checks original attributes BEFORE conversion, enabling detection of all mode switches: - color_temp → RGB ✓ - color_temp → XY ✓ - RGB → color_temp ✓ (NEW) - RGB → XY ✓ (NEW) - XY → color_temp ✓ (NEW) - XY → RGB ✓ (NEW) This improves on PR #1282 by detecting mode changes in both directions, not just from color_temp. Updated tests to cover all 6 mode transitions. --- custom_components/adaptive_lighting/switch.py | 87 ++++++++++++++----- tests/test_switch.py | 78 +++++++++++------ 2 files changed, 116 insertions(+), 49 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index b423fc3c..59b98a0c 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -694,6 +694,58 @@ def _add_missing_attributes( return old_attributes, new_attributes +def _has_color_mode_changed( + light: str, + old_attributes: dict[str, Any], + new_attributes: dict[str, Any], + context: Context, +) -> bool: + """Check if the light's color mode changed (e.g., color_temp to RGB or vice versa). + + This must be called BEFORE _add_missing_attributes() to detect mode changes + using the original attributes. See issue #1275. + """ + old_has_color_temp = old_attributes.get(ATTR_COLOR_TEMP_KELVIN) is not None + old_has_rgb = old_attributes.get(ATTR_RGB_COLOR) is not None + old_has_xy = old_attributes.get(ATTR_XY_COLOR) is not None + + new_has_color_temp = new_attributes.get(ATTR_COLOR_TEMP_KELVIN) is not None + new_has_rgb = new_attributes.get(ATTR_RGB_COLOR) is not None + new_has_xy = new_attributes.get(ATTR_XY_COLOR) is not None + + # Determine old and new color modes + # Priority: color_temp > rgb > xy (matching typical light behavior) + if old_has_color_temp: + old_mode = "color_temp" + elif old_has_rgb: + old_mode = "rgb" + elif old_has_xy: + old_mode = "xy" + else: + old_mode = None + + if new_has_color_temp: + new_mode = "color_temp" + elif new_has_rgb: + new_mode = "rgb" + elif new_has_xy: + new_mode = "xy" + else: + new_mode = None + + # Check if mode changed + if old_mode is not None and new_mode is not None and old_mode != new_mode: + _LOGGER.debug( + "Light mode of %s changed from %s to %s with context.id='%s'", + light, + old_mode, + new_mode, + context.id, + ) + return True + return False + + def _attributes_have_changed( light: str, old_attributes: dict[str, Any], @@ -706,6 +758,17 @@ def _attributes_have_changed( # so we must protect for `None` here # see https://github.com/home-assistant/core/pull/101946 + # Check for color mode changes BEFORE attribute conversion + # This detects external changes like Hue scenes switching from color_temp to RGB + # See: https://github.com/basnijholt/adaptive-lighting/issues/1275 + if adapt_color and _has_color_mode_changed( + light, + old_attributes, + new_attributes, + context, + ): + return True + if adapt_color: old_attributes, new_attributes = _add_missing_attributes( old_attributes, @@ -767,30 +830,6 @@ def _attributes_have_changed( ) return True - if adapt_color and ( - ( - old_attributes.get(ATTR_COLOR_TEMP_KELVIN) - and not new_attributes.get(ATTR_COLOR_TEMP_KELVIN) - ) - or ( - old_attributes.get(ATTR_RGB_COLOR) - and not new_attributes.get(ATTR_RGB_COLOR) - ) - ): - last_mode = ( - "color_temp" if old_attributes.get(ATTR_COLOR_TEMP_KELVIN) else "rgb" - ) - current_mode = ( - "color_temp" if new_attributes.get(ATTR_COLOR_TEMP_KELVIN) else "rgb" - ) - _LOGGER.debug( - "Light mode of %s changed from %s to %s with context.id='%s'", - light, - last_mode, - current_mode, - context.id, - ) - return True return False diff --git a/tests/test_switch.py b/tests/test_switch.py index 4d2ead62..5011ebc6 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -1014,9 +1014,8 @@ def test_attributes_have_changed(): # (e.g., when Hue scenes change light from color_temp to RGB mode) # See: https://github.com/basnijholt/adaptive-lighting/issues/1275 # - # Note: Due to _add_missing_attributes() converting colors to RGB for comparison, - # only switching FROM color_temp is reliably detected. Switching FROM rgb/xy - # may not be detected because new_attributes gets RGB added from conversion. + # All mode switches are now detected bidirectionally by checking original + # attributes BEFORE conversion in _has_color_mode_changed(). _LOGGER.debug( "Test switch from color_temp to rgb_color - should detect mode change", ) @@ -1025,12 +1024,10 @@ def test_attributes_have_changed(): new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, **kwargs, ) - # rgb → color_temp: NOT detected because new_attributes gets RGB added from - # color_temp conversion, so old still has RGB and new also has RGB _LOGGER.debug( - "Test switch from rgb_color to color_temp - not detected due to conversion", + "Test switch from rgb_color to color_temp - should detect mode change", ) - assert not _attributes_have_changed( + assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)}, new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, **kwargs, @@ -1041,12 +1038,8 @@ def test_attributes_have_changed(): new_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, **kwargs, ) - # xy → color_temp: Not detected by mode check, but may trigger RGB comparison - # if the converted RGB values differ significantly - _LOGGER.debug( - "Test switch from color_xy to color_temp - not detected by mode check", - ) - assert not _attributes_have_changed( + _LOGGER.debug("Test switch from color_xy to color_temp - should detect mode change") + assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)}, new_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702}, **kwargs, @@ -2369,18 +2362,16 @@ async def test_simple_switch_state_after_async_added_to_hass(hass): def test_attributes_have_changed_light_mode_switch(): - """Test detection of external light mode changes (color_temp vs rgb). + """Test detection of external light mode changes (color_temp vs rgb vs xy). Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1275 When a user activates a Hue Scene (or similar) via an external app, the light - may switch from color_temp mode to RGB/XY mode. This should be detected as an - external change so AL doesn't immediately override it. + may switch from color_temp mode to RGB/XY mode (or vice versa). This should be + detected as an external change so AL doesn't immediately override it. - Note: Due to _add_missing_attributes() converting colors to RGB for comparison, - only switching FROM color_temp is reliably detected by the mode check. - Switching FROM rgb/xy may not be detected by mode check because new_attributes - gets RGB added from conversion. + The _has_color_mode_changed() function checks the original attributes BEFORE + any conversion, enabling bidirectional mode change detection. """ context = Context() base_kwargs = { @@ -2389,25 +2380,50 @@ def test_attributes_have_changed_light_mode_switch(): "context": context, } - # Test 1: adapt_color=True - only FROM color_temp mode changes are detected + # Test 1: adapt_color=True - all mode changes should be detected kwargs_adapt_color = {**base_kwargs, "adapt_color": True} - # Switching FROM color_temp mode IS detected + # color_temp → RGB assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, **kwargs_adapt_color, ), "Should detect color_temp → RGB mode switch" + # color_temp → XY assert _attributes_have_changed( old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, **kwargs_adapt_color, ), "Should detect color_temp → XY mode switch" - # Switching FROM RGB/XY mode is NOT detected by mode check because - # new_attributes gets RGB added from color_temp/xy conversion - # (may still be detected by RGB value comparison if colors differ significantly) + # RGB → color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Should detect RGB → color_temp mode switch" + + # RGB → XY + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Should detect RGB → XY mode switch" + + # XY → color_temp + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_adapt_color, + ), "Should detect XY → color_temp mode switch" + + # XY → RGB + assert _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + **kwargs_adapt_color, + ), "Should detect XY → RGB mode switch" # No mode change - same type with same values shouldn't be detected assert not _attributes_have_changed( @@ -2422,6 +2438,12 @@ def test_attributes_have_changed_light_mode_switch(): **kwargs_adapt_color, ), "Same RGB should not be detected as change" + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_XY_COLOR: (0.64, 0.33)}, + **kwargs_adapt_color, + ), "Same XY should not be detected as change" + # Test 2: adapt_color=False - mode changes should NOT be detected kwargs_no_adapt = {**base_kwargs, "adapt_color": False} @@ -2430,3 +2452,9 @@ def test_attributes_have_changed_light_mode_switch(): new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, **kwargs_no_adapt, ), "Mode change should not be detected when adapt_color=False" + + assert not _attributes_have_changed( + old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)}, + new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000}, + **kwargs_no_adapt, + ), "RGB → color_temp should not be detected when adapt_color=False"