@@ -30,12 +30,14 @@ def _make_pipeline_result(
3030 position : int = 50 ,
3131 control_method : ControlMethod = ControlMethod .SOLAR ,
3232 bypass_auto_control : bool = False ,
33+ floor_clamp_applied : bool = False ,
3334) -> PipelineResult :
3435 return PipelineResult (
3536 position = position ,
3637 control_method = control_method ,
3738 reason = "test" ,
3839 bypass_auto_control = bypass_auto_control ,
40+ floor_clamp_applied = floor_clamp_applied ,
3941 )
4042
4143
@@ -396,6 +398,80 @@ async def test_trigger_outside_window_pre_start_no_command(self):
396398 coordinator ._cmd_svc .apply_position .assert_not_called ()
397399
398400
401+ class TestFloorClampUnderManualOverride :
402+ """A floor-clamp under an armed manual override must dispatch and not snap to default (#534)."""
403+
404+ @pytest .mark .asyncio
405+ async def test_floor_clamp_forces_dispatch_under_manual_override (self ):
406+ """A floor-clamped position under manual override calls context with force=True."""
407+ from custom_components .adaptive_cover_pro .coordinator import (
408+ AdaptiveDataUpdateCoordinator ,
409+ )
410+
411+ result = _make_pipeline_result (
412+ position = 80 ,
413+ control_method = ControlMethod .MANUAL ,
414+ floor_clamp_applied = True ,
415+ )
416+ coordinator = _make_coordinator (
417+ entities = ["cover.blind" ],
418+ pipeline_result = result ,
419+ )
420+ coordinator .manager .is_cover_manual .return_value = True
421+
422+ await AdaptiveDataUpdateCoordinator .async_handle_state_change (
423+ coordinator , state = 80 , options = {}
424+ )
425+
426+ coordinator ._build_position_context .assert_called_once_with (
427+ "cover.blind" ,
428+ {},
429+ force = True ,
430+ is_safety = False ,
431+ sun_just_appeared = coordinator ._check_sun_validity_transition .return_value ,
432+ )
433+
434+ @pytest .mark .asyncio
435+ async def test_floor_release_under_armed_override_does_not_force_to_default (self ):
436+ """Floor sensor off while override armed: stay at floor, no force to default (#534).
437+
438+ When the floor sensor releases and manual override is still the winner,
439+ the manual-hold winner re-emits its theoretical default (90). The
440+ coordinator must NOT take the custom_position_released force path —
441+ otherwise it would drive the cover to that default. force stays False.
442+ """
443+ from custom_components .adaptive_cover_pro .coordinator import (
444+ AdaptiveDataUpdateCoordinator ,
445+ )
446+
447+ result = _make_pipeline_result (
448+ position = 90 ,
449+ control_method = ControlMethod .MANUAL ,
450+ floor_clamp_applied = False ,
451+ )
452+ coordinator = _make_coordinator (
453+ entities = ["cover.blind" ],
454+ pipeline_result = result ,
455+ )
456+ coordinator .manager .is_cover_manual .return_value = True
457+ coordinator ._last_state_change_entity = "binary_sensor.cp1"
458+
459+ await AdaptiveDataUpdateCoordinator .async_handle_state_change (
460+ coordinator ,
461+ state = 90 ,
462+ options = {},
463+ custom_position_released_entities = {"binary_sensor.cp1" },
464+ )
465+
466+ coordinator ._build_position_context .assert_called_once_with (
467+ "cover.blind" ,
468+ {},
469+ force = False ,
470+ is_safety = False ,
471+ sun_just_appeared = coordinator ._check_sun_validity_transition .return_value ,
472+ )
473+
474+
399475class TestCustomPositionSensorReleaseEdgeBypassesGate :
400476 """Custom-position sensor release-edge mirrors force-override release (#365).
401477
0 commit comments