Compare commits

...

5 commits

Author SHA1 Message Date
Bas Nijholt
891a48e8e5
Add bidirectional color mode change detection (issue #1275)
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.
2025-11-27 09:48:23 -08:00
Bas Nijholt
9732877f55
Add tests for light mode change detection (issue #1275)
Update existing tests and add new test_attributes_have_changed_light_mode_switch
to verify the behavior of PR #1282 which detects when lights externally switch
from color_temp mode to RGB/XY mode.

Key test coverage:
- color_temp → RGB: detected ✓
- color_temp → XY: detected ✓
- RGB/XY → color_temp: not detected by mode check (but may trigger RGB comparison)
- Same color mode: not detected as change
- adapt_color=False: mode changes not detected
2025-11-27 09:43:44 -08:00
Bas Nijholt
537373987a
Merge remote-tracking branch 'origin/main' into external-from-kelvin-to-rgb-manual 2025-11-27 09:36:34 -08:00
pre-commit-ci[bot]
6bf89603e2 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2025-11-13 14:15:33 +00:00
DataGhost
22f3a2b4de Check for external light mode (temperature vs rgb) switch 2025-11-13 15:07:32 +01:00
2 changed files with 181 additions and 8 deletions

View file

@ -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,
@ -766,6 +829,7 @@ def _attributes_have_changed(
context.id,
)
return True
return False

View file

@ -1010,26 +1010,36 @@ def test_attributes_have_changed():
new_attributes=attrs,
**kwargs,
)
_LOGGER.debug("Test switch from color_temp to rgb_color")
assert not _attributes_have_changed(
# Test color mode switches - feature added to detect external changes
# (e.g., when Hue scenes change light from color_temp to RGB mode)
# See: https://github.com/basnijholt/adaptive-lighting/issues/1275
#
# 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",
)
assert _attributes_have_changed(
old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702},
new_attributes={ATTR_BRIGHTNESS: 1, ATTR_RGB_COLOR: (255, 166, 87)},
**kwargs,
)
_LOGGER.debug("Test switch from rgb_color to color_temp")
assert not _attributes_have_changed(
_LOGGER.debug(
"Test switch from rgb_color to color_temp - should detect mode change",
)
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,
)
_LOGGER.debug("Test switch from color_temp to color_xy")
assert not _attributes_have_changed(
_LOGGER.debug("Test switch from color_temp to color_xy - should detect mode change")
assert _attributes_have_changed(
old_attributes={ATTR_BRIGHTNESS: 1, ATTR_COLOR_TEMP_KELVIN: 2702},
new_attributes={ATTR_BRIGHTNESS: 1, ATTR_XY_COLOR: (0.526, 0.387)},
**kwargs,
)
_LOGGER.debug("Test switch from color_xy to color_temp")
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,
@ -2349,3 +2359,102 @@ async def test_simple_switch_state_after_async_added_to_hass(hass):
# State should still be correct after async_added_to_hass
assert switch_true._state is True
assert switch_false._state is False
def test_attributes_have_changed_light_mode_switch():
"""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 (or vice versa). This should be
detected as an external change so AL doesn't immediately override it.
The _has_color_mode_changed() function checks the original attributes BEFORE
any conversion, enabling bidirectional mode change detection.
"""
context = Context()
base_kwargs = {
"light": "light.test",
"adapt_brightness": True,
"context": context,
}
# Test 1: adapt_color=True - all mode changes should be detected
kwargs_adapt_color = {**base_kwargs, "adapt_color": True}
# 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"
# 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(
old_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000},
new_attributes={ATTR_BRIGHTNESS: 128, ATTR_COLOR_TEMP_KELVIN: 4000},
**kwargs_adapt_color,
), "Same color_temp should not be detected as change"
assert not _attributes_have_changed(
old_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)},
new_attributes={ATTR_BRIGHTNESS: 128, ATTR_RGB_COLOR: (255, 0, 0)},
**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}
assert not _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_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"