From c05587a092f0f3aeb109d4d69bc2f5c77cbc9d74 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Sun, 6 Sep 2026 01:31:45 -0700 Subject: [PATCH] Fix autoreset timeout during partial adaptation --- custom_components/adaptive_lighting/switch.py | 4 +- tests/test_switch.py | 81 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/custom_components/adaptive_lighting/switch.py b/custom_components/adaptive_lighting/switch.py index 44ad9c60..eb62d7b6 100644 --- a/custom_components/adaptive_lighting/switch.py +++ b/custom_components/adaptive_lighting/switch.py @@ -2497,9 +2497,11 @@ class AdaptiveLightingManager: if ( timer is not None and timer.is_running() + and not is_our_context(event.context) + and not self.is_proactively_adapting(event.context.id) and event.time_fired > timer.start_time # type: ignore[operator] ): - # Restart the auto reset timer + # Only external turn-ons extend manual control, not our adaptations. timer.start() if service == SERVICE_TURN_OFF: diff --git a/tests/test_switch.py b/tests/test_switch.py index b96e56fe..28e94492 100644 --- a/tests/test_switch.py +++ b/tests/test_switch.py @@ -895,10 +895,11 @@ async def test_manual_control( @flaky(max_runs=3, min_passes=1) -async def test_auto_reset_manual_control(hass): +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +async def test_auto_reset_manual_control(hass, mode): switch, (light, *_) = await setup_lights_and_switch( hass, - {CONF_AUTORESET_CONTROL: 0.1}, + {CONF_AUTORESET_CONTROL: 0.1, CONF_TAKE_OVER_CONTROL_MODE: mode}, ) context = switch.create_context("test") # needs to be passed to update method manual_control = switch.manager.manual_control @@ -965,6 +966,82 @@ async def test_auto_reset_manual_control(hass): assert not manual_control[light.entity_id] +@pytest.mark.parametrize("intercept", [False, True]) +@pytest.mark.parametrize("mode", list(TakeOverControlMode)) +@pytest.mark.parametrize( + ("attribute", "value", "next_value", "manual_attributes"), + [ + (ATTR_BRIGHTNESS, 10, 20, LightControlAttributes.BRIGHTNESS), + (ATTR_COLOR_TEMP_KELVIN, 2000, 2200, LightControlAttributes.COLOR), + ], +) +async def test_interval_adaptation_preserves_manual_control_timeout( + hass, + freezer, + cleanup, + intercept, + mode, + attribute, + value, + next_value, + manual_attributes, +): + """Adaptation must not postpone auto reset; another manual change must.""" + switch, (light, *_) = await setup_lights_and_switch( + hass, + { + CONF_AUTORESET_CONTROL: 7200, + CONF_TAKE_OVER_CONTROL_MODE: mode, + CONF_DETECT_NON_HA_CHANGES: False, + CONF_INTERCEPT: intercept, + }, + ) + + async def change_manually(value): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id, attribute: value}, + blocking=True, + ) + await hass.async_block_till_done() + + for manual_value in (value, next_value): + # A new external change to the already-manual axis restarts the timer. + await change_manually(manual_value) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + for elapsed in (90, 180): + freezer.tick(90) + await switch._async_update_at_interval_action() + await hass.async_block_till_done() + assert ( + switch.manager.get_manual_control_attributes(light.entity_id) + == manual_attributes + ) + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][ + light.entity_id + ] + == 7200 - elapsed + ) + + # An external bare turn-on also keeps its existing timer restart behavior. + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: light.entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert ( + switch.extra_state_attributes["autoreset_time_remaining"][light.entity_id] + == 7200 + ) + + async def test_adaptation_attribute_selection(hass): """Test the 'manual control' tracking.""" switch, (light, *_) = await setup_lights_and_switch(hass)