diff --git a/README.md b/README.md index 6f5a3715..7a7ee9d9 100644 --- a/README.md +++ b/README.md @@ -105,47 +105,47 @@ The YAML and frontend configuration methods support all of the options listed be -| Variable name | Description | Default | Type | -|:-------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| -| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | -| `interval` | Frequency to adapt the lights, in seconds. πŸ”„ | `90` | `int > 0` | -| `transition` | Duration of transition when lights change, in seconds. πŸ•‘ | `45` | `float` 0-6553 | -| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | -| `min_brightness` | Minimum brightness percentage. πŸ’‘ | `1` | `int` 1-100 | -| `max_brightness` | Maximum brightness percentage. πŸ’‘ | `100` | `int` 1-100 | -| `min_color_temp` | Warmest color temperature in Kelvin. πŸ”₯ | `2000` | `int` 1000-10000 | -| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | -| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | -| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | -| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. πŸŒ™ | `color_temp` | one of `['color_temp', 'rgb_color']` | -| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | -| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | -| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | -| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. πŸŒ™ | `False` | `bool` | -| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. πŸŒ… | `None` | `str` | -| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. πŸŒ… | `None` | `str` | -| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. πŸŒ… | `None` | `str` | -| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. πŸŒ‡ | `None` | `str` | -| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. πŸŒ‡ | `None` | `str` | -| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. πŸŒ‡ | `None` | `str` | -| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | -| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). πŸ“ˆ | `default` | one of `['default', 'linear', 'tanh']` | -| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. πŸ“ˆπŸ“‰ | `900` | `int` | -| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. πŸ“ˆπŸ“‰. | `3600` | `int` | -| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. πŸ”’ | `True` | `bool` | -| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | -| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. πŸ•΅οΈ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | -| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | -| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). πŸ”„ | `False` | `bool` | -| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. πŸ•΅οΈ | `False` | `bool` | -| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. πŸ”€ | `False` | `bool` | -| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | -| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | -| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. πŸ“‰Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | -| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | -| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. βž—βš οΈ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | -| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. πŸ“ | `False` | `bool` | +| Variable name | Description | Default | Type | +|:-------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------|:----------------------------------------| +| `lights` | List of light entity_ids to be controlled (may be empty). 🌟 | `[]` | list of `entity_id`s | +| `interval` | Frequency to adapt the lights, in seconds. πŸ”„ | `90` | `int > 0` | +| `transition` | Duration of transition when lights change, in seconds. πŸ•‘ | `45` | `float` 0-6553 | +| `initial_transition` | Duration of the first transition when lights turn from `off` to `on` in seconds. ⏲️ | `1` | `float` 0-6553 | +| `min_brightness` | Minimum brightness percentage. πŸ’‘ | `1` | `int` 1-100 | +| `max_brightness` | Maximum brightness percentage. πŸ’‘ | `100` | `int` 1-100 | +| `min_color_temp` | Warmest color temperature in Kelvin. πŸ”₯ | `2000` | `int` 1000-10000 | +| `max_color_temp` | Coldest color temperature in Kelvin. ❄️ | `5500` | `int` 1000-10000 | +| `prefer_rgb_color` | Whether to prefer RGB color adjustment over light color temperature when possible. 🌈 | `False` | `bool` | +| `sleep_brightness` | Brightness percentage of lights in sleep mode. 😴 | `1` | `int` 1-100 | +| `sleep_rgb_or_color_temp` | Use either `"rgb_color"` or `"color_temp"` in sleep mode. πŸŒ™ | `color_temp` | one of `['color_temp', 'rgb_color']` | +| `sleep_color_temp` | Color temperature in sleep mode (used when `sleep_rgb_or_color_temp` is `color_temp`) in Kelvin. 😴 | `1000` | `int` 1000-10000 | +| `sleep_rgb_color` | RGB color in sleep mode (used when `sleep_rgb_or_color_temp` is "rgb_color"). 🌈 | `[255, 56, 0]` | RGB color | +| `sleep_transition` | Duration of transition when "sleep mode" is toggled in seconds. 😴 | `1` | `float` 0-6553 | +| `transition_until_sleep` | When enabled, Adaptive Lighting will treat sleep settings as the minimum, transitioning to these values after sunset. πŸŒ™ | `False` | `bool` | +| `sunrise_time` | Set a fixed time (HH:MM:SS) for sunrise. πŸŒ… | `None` | `str` | +| `min_sunrise_time` | Set the earliest virtual sunrise time (HH:MM:SS), allowing for later sunrises. πŸŒ… | `None` | `str` | +| `max_sunrise_time` | Set the latest virtual sunrise time (HH:MM:SS), allowing for earlier sunrises. πŸŒ… | `None` | `str` | +| `sunrise_offset` | Adjust sunrise time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `sunset_time` | Set a fixed time (HH:MM:SS) for sunset. πŸŒ‡ | `None` | `str` | +| `min_sunset_time` | Set the earliest virtual sunset time (HH:MM:SS), allowing for later sunsets. πŸŒ‡ | `None` | `str` | +| `max_sunset_time` | Set the latest virtual sunset time (HH:MM:SS), allowing for earlier sunsets. πŸŒ‡ | `None` | `str` | +| `sunset_offset` | Adjust sunset time with a positive or negative offset in seconds. ⏰ | `0` | `int` | +| `brightness_mode` | Brightness mode to use. Possible values are `default`, `linear`, and `tanh` (uses `brightness_mode_time_dark` and `brightness_mode_time_light`). πŸ“ˆ | `default` | one of `['default', 'linear', 'tanh']` | +| `brightness_mode_time_dark` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness before/after sunrise/sunset. πŸ“ˆπŸ“‰ | `900` | `int` | +| `brightness_mode_time_light` | (Ignored if `brightness_mode='default'`) The duration in seconds to ramp up/down the brightness after/before sunrise/sunset. πŸ“ˆπŸ“‰. | `3600` | `int` | +| `take_over_control` | Pause adaptation of individual lights and hand over (manual) control to other sources that issue `light.turn_on` calls for lights that are on. πŸ”’ | `True` | `bool` | +| `take_over_control_mode` | The adaptation pausing mode when other sources change brightness and/or color of lights. `pause_all` always pauses both brightness and color adaptation. `pause_changed` pauses the adaptation of only the changed attributes and continues adapting unchanged attributes, e.g., continues color adaptation when only brightness was changed. | `pause_all` | one of `['pause_all', 'pause_changed']` | +| `detect_non_ha_changes` | Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. πŸ•΅οΈ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues. | `False` | `bool` | +| `autoreset_control_seconds` | Automatically reset the manual control after a number of seconds. Set to 0 to disable. ⏲️ | `0` | `int` 0-31536000 | +| `only_once` | Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). πŸ”„ | `False` | `bool` | +| `adapt_only_on_bare_turn_on` | When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. πŸ•΅οΈ | `False` | `bool` | +| `separate_turn_on_commands` | Use separate `light.turn_on` calls for color and brightness, needed for some light types. πŸ”€ | `False` | `bool` | +| `send_split_delay` | Delay (ms) between `separate_turn_on_commands` for lights that don't support simultaneous brightness and color setting. ⏲️ | `0` | `int` 0-10000 | +| `adapt_delay` | Wait time (seconds) between light turn on and Adaptive Lighting applying changes. Might help to avoid flickering. ⏲️ | `0` | `float > 0` | +| `skip_redundant_commands` | Skip sending adaptation commands whose target state already equals the light's known state. Minimizes network traffic and improves the adaptation responsivity in some situations. πŸ“‰Disable if physical light states get out of sync with HA's recorded state. | `False` | `bool` | +| `intercept` | Intercept and adapt `light.turn_on` calls to enabling instantaneous color and brightness adaptation. 🏎️ Disable for lights that do not support `light.turn_on` with color and brightness. | `True` | `bool` | +| `multi_light_intercept` | Intercept and adapt `light.turn_on` calls that target multiple lights. βž—βš οΈ This might result in splitting up a single `light.turn_on` call into multiple calls, e.g., when lights are in different switches. Requires `intercept` to be enabled. | `True` | `bool` | +| `include_config_in_attributes` | Show all options as attributes on the switch in Home Assistant when set to `true`. πŸ“ | `False` | `bool` | diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 37a9ba8b..502318f0 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -95,7 +95,7 @@ CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON = ( DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = ( "When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is " "invoked without specifying color or brightness. ❌🌈 " - "This e.g., prevents adaptation when activating a scene. " + "This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. " "If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. " "Needs `take_over_control` enabled. πŸ•΅οΈ" ) diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index 6985a94e..4b18ea06 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -57,7 +57,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. πŸ•΅οΈ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). πŸ”„", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. πŸ•΅οΈ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. πŸ•΅οΈ", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. πŸ”€", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 3ded833e..3923e056 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -1545,13 +1545,13 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): service_data, ): _LOGGER.debug( - "Skipping responding to 'off' β†’ 'on' event for '%s' with context.id='%s' because" - " we only adapt on bare `light.turn_on` events and not on service_data: '%s'", + "Marked attributes from service_data as manually controlled for '%s' " + "with context.id='%s'. Continuing to adapt remaining attributes. " + "service_data: '%s'", entity_id, event.context.id, service_data, ) - return if self._adapt_delay > 0: await asyncio.sleep(self._adapt_delay) @@ -2216,7 +2216,7 @@ class AdaptiveLightingManager: "Light %s: Setting manual control attributes to %s (from %s).", light, attributes, - self.manual_control[light], + self.get_manual_control_attributes(light), ) self.manual_control[light] = attributes delay = self.auto_reset_manual_control_times.get(light) @@ -2392,22 +2392,28 @@ class AdaptiveLightingManager: task.cancel() self.turn_on_event[eid] = event - try: - switch = _switch_with_lights( - self.hass, - [eid], - expand_light_groups=False, - ) - await self.update_manually_controlled_from_event( - switch, - eid, - force=False, - ) - except NoSwitchFoundError: - _LOGGER.debug( - "No switch found for entity_id='%s' in 'on' event listener", - eid, - ) + # Only check for manual control via this path if the light was already ON. + # Turning on from OFF is handled separately in _respond_to_off_to_on_event, + # where adapt_only_on_bare_turn_on can mark lights as manually controlled. + # Fix for https://github.com/basnijholt/adaptive-lighting/issues/1378 + state = self.hass.states.get(eid) + if state is not None and state.state == STATE_ON: + try: + switch = _switch_with_lights( + self.hass, + [eid], + expand_light_groups=False, + ) + await self.update_manually_controlled_from_event( + switch, + eid, + force=False, + ) + except NoSwitchFoundError: + _LOGGER.debug( + "No switch found for entity_id='%s' in 'on' event listener", + eid, + ) timer = self.auto_reset_manual_control_timers.get(eid) if ( @@ -2851,6 +2857,12 @@ class AdaptiveLightingManager: entity_id: str, service_data: ServiceData, ) -> bool: + """Mark light as manually controlled if turn_on call has brightness/color attributes. + + This is used by adapt_only_on_bare_turn_on to mark lights as manually controlled + when they are turned on with specific attributes (e.g., from a scene). + This ensures scenes persist and AL doesn't override them. + """ _LOGGER.debug( "_mark_manual_control_if_non_bare_turn_on: entity_id='%s', service_data='%s'", entity_id, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 1e5a1fb9..e39898fd 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -58,7 +58,7 @@ "detect_non_ha_changes": "detect_non_ha_changes: Detects and halts adaptations for non-`light.turn_on` state changes. Needs `take_over_control` enabled. πŸ•΅οΈ Caution: ⚠️ Some lights might falsely indicate an 'on' state, which could result in lights turning on unexpectedly. Note that this calls `homeassistant.update_entity` every `interval`! Disable this feature if you encounter such issues.", "autoreset_control_seconds": "autoreset_control_seconds", "only_once": "only_once: Adapt lights only when they are turned on (`true`) or keep adapting them (`false`). πŸ”„", - "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. πŸ•΅οΈ", + "adapt_only_on_bare_turn_on": "adapt_only_on_bare_turn_on: When turning lights on initially. If set to `true`, AL adapts only if `light.turn_on` is invoked without specifying color or brightness. ❌🌈 This e.g., prevents adaptation when activating a scene and marks the light as manually controlled. If `false`, AL adapts regardless of the presence of color or brightness in the initial `service_data`. Needs `take_over_control` enabled. πŸ•΅οΈ", "separate_turn_on_commands": "separate_turn_on_commands: Use separate `light.turn_on` calls for color and brightness, needed for some light types. πŸ”€", "send_split_delay": "send_split_delay", "adapt_delay": "adapt_delay", diff --git a/tests/test_switch.py b/tests/test_switch.py index 3810bdbe..5ba3cb60 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -47,6 +47,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_SUNRISE_TIME, CONF_SUNSET_TIME, CONF_TAKE_OVER_CONTROL, + CONF_TAKE_OVER_CONTROL_MODE, CONF_TRANSITION, CONF_TURN_ON_LIGHTS, CONF_USE_DEFAULTS, @@ -705,6 +706,21 @@ async def test_manual_control( assert not manual_control[ENTITY_LIGHT_1], manual_control await turn_light(True, brightness=increased_brightness()) assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + # Turning on from OFF with brightness: + # - With adapt_only_on_bare_turn_on=True: SHOULD mark as manually controlled (to preserve scenes) + # - With adapt_only_on_bare_turn_on=False: should NOT mark (fix for issue #1378) + if adapt_only_on_bare_turn_on: + assert ( + manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS + ), manual_control + else: + assert not manual_control[ENTITY_LIGHT_1], manual_control + # Reset for next test + await turn_light(False) + await turn_light(True) + assert not manual_control[ENTITY_LIGHT_1], manual_control + # Now change brightness while ON - this should always be manual control + await turn_light(True, brightness=increased_brightness()) assert ( manual_control[ENTITY_LIGHT_1] == LightControlAttributes.BRIGHTNESS ), manual_control @@ -2746,3 +2762,155 @@ async def test_skipped_lights_context_not_from_arbitrary_switch(hass): f"but got {name_hash_in_context}. This indicates the context is still " f"being created from an arbitrary switch instead of the manager." ) + + +async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass): + """Test that turning on a light from OFF via automation is not marked as manual control. + + Regression test for https://github.com/basnijholt/adaptive-lighting/issues/1378 + + When an automation turns on a light from OFF state with brightness/color attributes, + the light should NOT be marked as manually controlled. Adaptive Lighting should + adapt the light normally. + + The bug in v1.30.0 was that `update_manually_controlled_from_event` was called for + ALL `light.turn_on` events, not just when the light was already ON. This caused + lights turned on by automations to be incorrectly marked as "manually controlled". + """ + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_TAKE_OVER_CONTROL: True, + CONF_DETECT_NON_HA_CHANGES: False, + }, + ) + + # Ensure light is OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + # Verify light is not manually controlled + assert not switch.manager.manual_control.get( + ENTITY_LIGHT_1, + ), "Light should not be manually controlled before test" + + # Simulate an automation turning on the light with brightness + # This is an external call (not from AL) with brightness attribute + external_context = Context(id="automation_context_12345") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 255, + }, + blocking=True, + context=external_context, + ) + await hass.async_block_till_done() + + # The light should be ON + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + # CRITICAL: The light should NOT be marked as manually controlled! + # The bug in v1.30.0 would incorrectly mark this as manual control because + # the turn_on had a brightness attribute. + manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1) + assert not manual_control_attrs, ( + f"Bug confirmed: Light was incorrectly marked as manually controlled " + f"(attributes: {manual_control_attrs}) when turned on from OFF state. " + f"Lights turned on from OFF by automations should NOT be marked as " + f"manually controlled - only lights that were already ON and then had " + f"their brightness/color changed externally should be marked as such." + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +async def test_adapt_only_on_bare_turn_on_respects_pause_changed_mode(hass, intercept): + """Test that adapt_only_on_bare_turn_on respects take_over_control_mode=PAUSE_CHANGED. + + When adapt_only_on_bare_turn_on=True and take_over_control_mode=PAUSE_CHANGED, + turning on a light from OFF with only brightness should: + 1. Mark ONLY brightness as manually controlled (not all attributes) + 2. Continue adapting color (since only brightness was specified) + + This test verifies the integration of #1356 (individual attribute tracking) + with adapt_only_on_bare_turn_on. Prior to the fix, the code would return early + after marking attributes as manually controlled, skipping all adaptation + including unspecified attributes like color. + + The test is parameterized with intercept=True/False to verify consistency + between the intercept path and the reactive (event-based) path. + """ + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_TAKE_OVER_CONTROL: True, + CONF_TAKE_OVER_CONTROL_MODE: TakeOverControlMode.PAUSE_CHANGED.value, + CONF_ADAPT_ONLY_ON_BARE_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: intercept, + }, + ) + + # Verify settings + assert switch._take_over_control + assert switch._take_over_control_mode == TakeOverControlMode.PAUSE_CHANGED + assert switch._adapt_only_on_bare_turn_on + + # Ensure light is OFF + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + + # Clear any prior service data + switch.manager.last_service_data.pop(ENTITY_LIGHT_1, None) + + # Turn on light from OFF with only brightness (simulating a scene or automation) + external_context = Context(id="scene_turn_on_with_brightness") + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ENTITY_LIGHT_1, + ATTR_BRIGHTNESS: 200, # Only brightness specified + }, + blocking=True, + context=external_context, + ) + await hass.async_block_till_done() + + # Light should be ON + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_ON + + # 1. Verify that ONLY brightness is marked as manually controlled + manual_control_attrs = switch.manager.manual_control.get(ENTITY_LIGHT_1) + assert manual_control_attrs == LightControlAttributes.BRIGHTNESS, ( + f"Expected only BRIGHTNESS to be marked as manually controlled, " + f"but got: {manual_control_attrs}. With adapt_only_on_bare_turn_on=True, " + f"only the attributes specified in the turn_on call should be marked." + ) + + # 2. Verify that color WAS adapted (last_service_data should have color_temp) + last_service_data = switch.manager.last_service_data.get(ENTITY_LIGHT_1) + assert last_service_data is not None, ( + "Bug: last_service_data is None, meaning adaptation was skipped entirely. " + "With PAUSE_CHANGED mode, color should still be adapted since only brightness " + "was marked as manually controlled." + ) + assert ATTR_COLOR_TEMP_KELVIN in last_service_data, ( + f"Bug: Color was not adapted. last_service_data={last_service_data}. " + f"With take_over_control_mode=PAUSE_CHANGED and only brightness marked " + f"as manually controlled, color_temp should still be adapted." + )