From 9e29a21197deeeba9a5eb96ad2da08e224fd7286 Mon Sep 17 00:00:00 2001 From: Alistair Galbraith Date: Sun, 6 Sep 2026 12:02:30 -0700 Subject: [PATCH] Add manual_control_on_external_turn_on option (#1490) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add `adapt_only_on_ha_turn_on` to skip adapting externally turned-on lights When a light turns on from `off` via a source outside Home Assistant — a physical wall switch or a hub/manufacturer scene (e.g. Lutron) — and `detect_non_ha_changes` is enabled, Adaptive Lighting adapts the light on the resulting `off` → `on` event, overriding the brightness/color the external source just set. Disabling `detect_non_ha_changes` avoids this but also stops detection of manual changes to already-on lights; the two behaviors were coupled to a single flag. Add `adapt_only_on_ha_turn_on` (default `false`, requires `take_over_control`). When enabled, an `off` → `on` transition with no matching HA `light.turn_on` context is marked `manual_control` and left untouched, independent of `detect_non_ha_changes`, decoupling the two behaviors. The off→on guard reduces to the previous expression when the option is `false`, so existing configurations are unaffected. Includes a parametrized regression test, docs, and regenerated strings/services/README via scripts/update-generated-content. Refs #435 Co-Authored-By: Claude Opus 4.8 * Shorten generated turn-on option description * Document shared turn-on policy limitations * Name external turn-on policy after manual-control behavior * Clarify settings needed to adapt unmatched turn-ons --------- Co-authored-by: Claude Opus 4.8 Co-authored-by: Bas Nijholt --- README.md | 3 + custom_components/adaptive_lighting/const.py | 15 ++ .../adaptive_lighting/services.yaml | 6 + .../adaptive_lighting/strings.json | 5 + custom_components/adaptive_lighting/switch.py | 30 +++- .../adaptive_lighting/translations/en.json | 5 + docs/advanced/manual-control.md | 22 +++ docs/configuration.md | 1 + docs/troubleshooting.md | 2 + tests/test_config_flow.py | 6 + tests/test_switch.py | 147 ++++++++++++++++++ 11 files changed, 235 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 1ee91993..d15c5249 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,7 @@ The YAML and frontend configuration methods support all of the options listed be | `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` | +| `manual_control_on_external_turn_on` | Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | | `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `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 | @@ -769,6 +770,8 @@ Addressing these issues will significantly improve your Home Assistant experienc In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. +To keep detecting manual changes to lights that are already on while leaving unmatched `off` to `on` state events unchanged, enable `manual_control_on_external_turn_on`. Matching uses the exact context of the most recently recorded `light.turn_on` call. Some integrations replace or omit that context, so Adaptive Lighting cannot distinguish every physical versus Home Assistant turn-on source. + #### :signal_strength: WiFi Networks Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. diff --git a/custom_components/adaptive_lighting/const.py b/custom_components/adaptive_lighting/const.py index 58b37734..7acf2781 100644 --- a/custom_components/adaptive_lighting/const.py +++ b/custom_components/adaptive_lighting/const.py @@ -100,6 +100,16 @@ DOCS[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] = ( "Needs `take_over_control` enabled. 🕵️" ) +CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON = ( + "manual_control_on_external_turn_on", + False, +) +DOCS[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] = ( + "Treat turn-ons without a matching Home Assistant `light.turn_on` context as " + "manual control. Normal manual-control resets apply. Still allows " + "`detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️" +) + CONF_PREFER_RGB_COLOR, DEFAULT_PREFER_RGB_COLOR = "prefer_rgb_color", False DOCS[CONF_PREFER_RGB_COLOR] = ( "Whether to prefer RGB color adjustment over " @@ -416,6 +426,11 @@ VALIDATION_TUPLES: list[tuple[str, Any, Any]] = [ ), (CONF_ONLY_ONCE, DEFAULT_ONLY_ONCE, bool), (CONF_ADAPT_ONLY_ON_BARE_TURN_ON, DEFAULT_ADAPT_ONLY_ON_BARE_TURN_ON, bool), + ( + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, + bool, + ), ( CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, DEFAULT_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE, diff --git a/custom_components/adaptive_lighting/services.yaml b/custom_components/adaptive_lighting/services.yaml index 2471e83a..23e8ceda 100644 --- a/custom_components/adaptive_lighting/services.yaml +++ b/custom_components/adaptive_lighting/services.yaml @@ -238,6 +238,12 @@ change_switch_settings: example: false selector: boolean: null + manual_control_on_external_turn_on: + description: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ + required: false + example: false + selector: + boolean: null transition: description: Duration of transition when lights change, in seconds. 🕑 required: false diff --git a/custom_components/adaptive_lighting/strings.json b/custom_components/adaptive_lighting/strings.json index ff576a2b..6e07c69d 100644 --- a/custom_components/adaptive_lighting/strings.json +++ b/custom_components/adaptive_lighting/strings.json @@ -70,6 +70,7 @@ "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 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. 🕵️", + "manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", "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", @@ -270,6 +271,10 @@ "description": "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.", "name": "detect_non_ha_changes" }, + "manual_control_on_external_turn_on": { + "description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "name": "manual_control_on_external_turn_on" + }, "transition": { "description": "Duration of transition when lights change, in seconds. 🕑", "name": "transition" diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 0a340c6c..ab9273df 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -104,6 +104,7 @@ from .const import ( CONF_INTERVAL, CONF_LIGHTS, CONF_MANUAL_CONTROL, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, CONF_MAX_SUNRISE_TIME, @@ -955,12 +956,14 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): self._send_split_delay = data[CONF_SEND_SPLIT_DELAY] self._take_over_control = data[CONF_TAKE_OVER_CONTROL] if not data[CONF_TAKE_OVER_CONTROL] and ( - data[CONF_DETECT_NON_HA_CHANGES] or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + data[CONF_DETECT_NON_HA_CHANGES] + or data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + or data[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] ): _LOGGER.warning( - "%s: Config mismatch: `detect_non_ha_changes` or `adapt_only_on_bare_turn_on` " - "set to `true` requires `take_over_control` to be enabled. Adjusting config " - "and continuing setup with `take_over_control: true`.", + "%s: Config mismatch: `detect_non_ha_changes`, `adapt_only_on_bare_turn_on`, " + "or `manual_control_on_external_turn_on` set to `true` requires `take_over_control` to be " + "enabled. Adjusting config and continuing setup with `take_over_control: true`.", self._name, ) self._take_over_control = True @@ -969,6 +972,9 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) self._detect_non_ha_changes = data[CONF_DETECT_NON_HA_CHANGES] self._adapt_only_on_bare_turn_on = data[CONF_ADAPT_ONLY_ON_BARE_TURN_ON] + self._manual_control_on_external_turn_on = data[ + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON + ] self._auto_reset_manual_control_time = data[CONF_AUTORESET_CONTROL] self._reset_manual_control_on_sleep_mode_change = data[ CONF_RESET_MANUAL_CONTROL_ON_SLEEP_MODE_CHANGE @@ -1603,16 +1609,26 @@ class AdaptiveSwitch(SwitchEntity, RestoreEntity): ) if ( self._take_over_control - and not self._detect_non_ha_changes + and ( + not self._detect_non_ha_changes + or self._manual_control_on_external_turn_on + ) and not from_turn_on ): # There is an edge case where 2 switches control the same light, e.g., # one for brightness and one for color. Now we will mark both switches # as manually controlled, which is not 100% correct. + # + # This 'off' → 'on' event does not exactly match the most recently tracked + # `light.turn_on` context for the entity. Hand control over when either: + # - `detect_non_ha_changes` is False (we can't reliably track manual changes + # to already-on lights anyway), or + # - `manual_control_on_external_turn_on` is True (the user explicitly wants external + # turn-ons left untouched, even while `detect_non_ha_changes` is enabled). _LOGGER.debug( "%s: Ignoring 'off' → 'on' event for '%s' with context.id='%s'" - " because 'light.turn_on' was not called by HA and" - " 'detect_non_ha_changes' is False", + " because it does not match a tracked 'light.turn_on' context and" + " ('detect_non_ha_changes' is False or 'manual_control_on_external_turn_on' is True)", self._name, entity_id, event.context.id, diff --git a/custom_components/adaptive_lighting/translations/en.json b/custom_components/adaptive_lighting/translations/en.json index 688bb984..2f1a7085 100644 --- a/custom_components/adaptive_lighting/translations/en.json +++ b/custom_components/adaptive_lighting/translations/en.json @@ -71,6 +71,7 @@ "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 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. 🕵️", + "manual_control_on_external_turn_on": "manual_control_on_external_turn_on: Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", "reset_manual_control_on_sleep_mode_change": "reset_manual_control_on_sleep_mode_change: Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴", "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", @@ -271,6 +272,10 @@ "description": "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.", "name": "detect_non_ha_changes" }, + "manual_control_on_external_turn_on": { + "description": "Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️", + "name": "manual_control_on_external_turn_on" + }, "transition": { "description": "Duration of transition when lights change, in seconds. 🕑", "name": "transition" diff --git a/docs/advanced/manual-control.md b/docs/advanced/manual-control.md index 4e84ca3f..dc099c57 100644 --- a/docs/advanced/manual-control.md +++ b/docs/advanced/manual-control.md @@ -112,6 +112,28 @@ adaptive_lighting: adapt_only_on_bare_turn_on: true ``` +### manual_control_on_external_turn_on + +When enabled, a turn-on without a state-change context matching the latest recorded Home Assistant `light.turn_on` is treated as manual control. This pauses brightness and color adaptation until manual control resets, rather than skipping just the first adjustment. The usual off/on, explicit reset, and configured timeout rules apply. A later unmatched turn-on marks the light manually controlled again. + +Manual-control flags are shared by profiles controlling the same light. Use the same turn-on policy on those profiles; mixed policies can allow an earlier profile to adapt before another marks the light manually controlled. + +Enable this if you want turn-ons from physical controls or native scenes to preserve their brightness and color. To adapt unmatched turn-ons, leave this disabled and enable `detect_non_ha_changes`. + +Its advantage over simply disabling `detect_non_ha_changes` is that the two behaviors are decoupled: you can keep `detect_non_ha_changes: true` to catch manual dimming of lights that are *already on*, while leaving unmatched turn-ons untouched. + +Adaptive Lighting cannot identify every physical versus Home Assistant source. Some integrations replace or omit the service context when they publish device state. In that case, even a Home Assistant turn-on does not match and this option treats it as external. + +```yaml +adaptive_lighting: + - name: "Respect physical switches and Lutron scenes" + lights: + - light.living_room + take_over_control: true + detect_non_ha_changes: true # still catch manual changes to already-on lights + manual_control_on_external_turn_on: true # leave unmatched off→on events unchanged +``` + ## Checking Manual Control Status You can see which lights are marked as manually controlled by checking the switch attributes: diff --git a/docs/configuration.md b/docs/configuration.md index 7edaac57..2a7402f0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -66,6 +66,7 @@ All configuration options are listed below with their default values. These opti | `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` | +| `manual_control_on_external_turn_on` | Treat turn-ons without a matching Home Assistant `light.turn_on` context as manual control. Normal manual-control resets apply. Still allows `detect_non_ha_changes` for already-on lights. Needs `take_over_control` enabled. 🕵️ | `False` | `bool` | | `reset_manual_control_on_sleep_mode_change` | Reset manual control when the sleep mode switch is toggled. Set to `false` to preserve manual control across sleep mode changes. 😴 | `True` | `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 | diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index c5f59353..f39c62bc 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -59,6 +59,8 @@ Addressing these issues will significantly improve your Home Assistant experienc In case lights are suddenly turning on by themselves, this is most likely due to the light incorrectly reporting an "on" state to Home Assistant, leading to an undesired Adaptive Lighting action. To prevent adapting in cases *where the state of the light is suddenly "on" and only adapt if there is an associated `light.turn_on` service call*, set `detect_non_ha_changes: false`. +To keep detecting manual changes to lights that are already on while leaving unmatched `off` to `on` state events unchanged, enable `manual_control_on_external_turn_on`. Matching uses the exact context of the most recently recorded `light.turn_on` call. Some integrations replace or omit that context, so Adaptive Lighting cannot distinguish every physical versus Home Assistant turn-on source. + #### :signal_strength: WiFi Networks Ensure your light bulbs have a strong WiFi connection. If the signal strength is less than -70dBm, the connection may be weak and prone to dropping messages. diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index 04cbed5b..241bd5ae 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -12,8 +12,10 @@ except ImportError: from homeassistant.components.adaptive_lighting.const import ( BASIC_OPTIONS, CONF_INITIAL_TRANSITION, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, CONF_SUNRISE_TIME, CONF_SUNSET_TIME, + DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, DEFAULT_NAME, DOMAIN, NONE_STR, @@ -149,6 +151,10 @@ async def test_options_schema_has_each_setting_once(hass): advanced = _advanced_section(result) assert advanced.options == {"collapsed": True} + assert ( + _schema_defaults(advanced.schema)[CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON] + is DEFAULT_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON + ) assert {key.schema for key in schema if key.schema != "advanced"} == BASIC_OPTIONS assert {key.schema for key in advanced.schema.schema} == set( DEFAULT_DATA, diff --git a/tests/test_switch.py b/tests/test_switch.py index cfebc4f6..1765fb5e 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -39,6 +39,7 @@ from homeassistant.components.adaptive_lighting.const import ( CONF_DETECT_NON_HA_CHANGES, CONF_INITIAL_TRANSITION, CONF_MANUAL_CONTROL, + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON, CONF_MAX_BRIGHTNESS, CONF_MAX_COLOR_TEMP, CONF_MIN_BRIGHTNESS, @@ -4473,6 +4474,152 @@ async def test_automation_turn_on_from_off_not_marked_as_manual_control(hass): ) +@pytest.mark.parametrize("intercept", [True, False]) +async def test_manual_control_on_external_turn_on_allows_tracked_service_call( + hass, + intercept, +): + """Test a real HA turn-on remains eligible for initial adaptation.""" + switch, _ = await setup_lights_and_switch( + hass, + { + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1}, + blocking=True, + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_LIGHT_1, ATTR_BRIGHTNESS: 200}, + blocking=True, + context=Context(id=f"ha_turn_on_{intercept}"), + ) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_LIGHT_1) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 128 + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.NONE + ) + + +@pytest.mark.parametrize("intercept", [True, False]) +@pytest.mark.parametrize( + ( + "manual_control_on_external_turn_on", + "detect_non_ha_changes", + "expected_manual_control", + "expected_adaptation", + ), + [ + (True, True, LightControlAttributes.ALL, False), + (True, False, LightControlAttributes.ALL, False), + (False, True, LightControlAttributes.NONE, True), + (False, False, LightControlAttributes.ALL, False), + ], +) +async def test_manual_control_on_external_turn_on_external_state_change( + hass, + freezer, + intercept, + manual_control_on_external_turn_on, + detect_non_ha_changes, + expected_manual_control, + expected_adaptation, +): + """Test an unmatched off-to-on state event follows the opt-in policy.""" + switch, _ = await setup_lights_and_switch( + hass, + { + "manual_control_on_external_turn_on": manual_control_on_external_turn_on, + CONF_DETECT_NON_HA_CHANGES: detect_non_ha_changes, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + external_attributes = dict(hass.states.get(ENTITY_LIGHT_1).attributes) + external_attributes[ATTR_BRIGHTNESS] = 200 + hass.states.async_set( + ENTITY_LIGHT_1, + STATE_OFF, + external_attributes, + context=Context(id=f"unmatched_turn_off_{intercept}"), + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).state == STATE_OFF + freezer.tick(6) + + hass.states.async_set( + ENTITY_LIGHT_1, + STATE_ON, + external_attributes, + context=Context(id=f"unmatched_turn_on_{intercept}"), + ) + await hass.async_block_till_done() + + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == expected_manual_control + ) + last_service_data = switch.manager.last_service_data.get(ENTITY_LIGHT_1) + if expected_adaptation: + assert last_service_data[ATTR_BRIGHTNESS] == 128 + else: + assert last_service_data is None + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200 + + +@pytest.mark.parametrize("intercept", [True, False]) +async def test_manual_control_on_external_turn_on_keeps_non_ha_change_detection( + hass, + intercept, +): + """Test the option does not disable manual tracking for an on light.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_MANUAL_CONTROL_ON_EXTERNAL_TURN_ON: True, + CONF_DETECT_NON_HA_CHANGES: True, + CONF_INTERCEPT: intercept, + CONF_MIN_BRIGHTNESS: 50, + CONF_MAX_BRIGHTNESS: 50, + }, + ) + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + transition=0, + ) + await hass.async_block_till_done() + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 128 + + set_light_brightness(light, 200) + light.async_write_ha_state() + await switch._update_attrs_and_maybe_adapt_lights( + context=switch.create_context("test"), + transition=0, + ) + await hass.async_block_till_done() + + assert hass.states.get(ENTITY_LIGHT_1).attributes[ATTR_BRIGHTNESS] == 200 + assert ( + switch.manager.get_manual_control_attributes(ENTITY_LIGHT_1) + == LightControlAttributes.BRIGHTNESS + ) + + @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.