Phase 4: Timed Calibration (v1.3.0)#6
Merged
Conversation
…cture - Verify module importability and class existence - Verify CAL_MAX_TRAVEL_TIME=120 and CAL_MIN_TRAVEL_TIME=2 constants - Verify all required async_step_* methods present - Verify no CMD_STOP in module (D-06) - Verify time.monotonic() used, not time.time() (D-07)
- const.py: add CAL_MAX_TRAVEL_TIME=120 and CAL_MIN_TRAVEL_TIME=2 (D-08/D-09) - options_flow_timed_calibration.py: TimedCalibrationFlowHandler with: - async_step_timed_cal_precondition: instruction form, no drive command (D-05) - async_step_timed_cal_close: CMD_DOWN + monotonic timing + 2s/120s guards (D-04/D-07) - async_step_timed_cal_open: CMD_UP + monotonic timing + same guards (D-04/D-07) - async_step_timed_cal_confirm: show measured times, redo path (D-10) - _emit_calibration_signal: SIGNAL_CALIBRATION_COMPLETED with final_position=100 (D-14) - No CMD_STOP (D-06), no asyncio.Event/wait_for (SC#1 by construction) - start time recorded BEFORE await control_blind (Phase 3 locked rule) - Fix test assertions to check import namespace rather than source text - ruff clean, mypy clean
config_flow.py:
- Import TimedCalibrationFlowHandler
- Add self.timed_cal_handler and _get_timed_cal_handler() lazy getter
- async_step_reconfigure: route timed motors to TimedCalibrationFlowHandler
(inverted from abort) while bidirectional path unchanged (CTRL-05)
- Add 4 delegate steps: async_step_timed_cal_{precondition,close,open,confirm}
so HA does not raise UnknownStep when form step_ids are rendered
strings.json + translations/en.json:
- 4 new step keys: timed_cal_{precondition,close,open,confirm}
- 2 new error keys: timed_cal_too_short, timed_cal_too_long
(both include repositioning guidance per REVIEW-2)
- confirm step redo label includes reopening instruction
translations/de.json, es.json, fr.json:
- English placeholder strings for the 4 new step keys and 2 error keys
ruff clean, mypy clean, all 5 JSON files parse
tests/test_timed_calibration_flow.py: - test_timed_cal_precondition_shows_form: routing + precondition form (D-01/D-05) - test_timed_cal_close_sends_cmd_down_records_start: CMD_DOWN + start time (D-04/D-07) - test_timed_cal_close_too_short: 2s floor guard (D-09) - test_timed_cal_close_too_long: 120s cap guard (D-08) - test_timed_cal_guard_reshow_does_not_redrive: no re-drive on guard (REVIEW-3) - test_timed_cal_no_stop_on_end_press: CMD_STOP never sent (D-06) - test_timed_cal_happy_path_reaches_confirm: full close+open -> confirm (D-04/D-10) - test_timed_cal_confirm_emits_signal_with_100: signal with final_position=100 (D-14) - test_timed_cal_redo_returns_to_precondition: redo path resets and loops (D-10) - test_reconfigure_bidirectional_routes_to_legacy: CTRL-05 not regressed - test_timed_cal_abort_emits_no_signal: mid-flow abort emits no signal (D-11) tests/test_config_flow.py: - Rename test_reconfigure_timed_motor_aborts -> test_reconfigure_timed_motor_enters_timed_flow - Assert timed reconfigure enters timed_cal_precondition (not abort) per REVIEW-4
- Add final_position: int = 0 param to _handle_calibration_completed - Branch end-state on final_position (timed -> 100% open; legacy -> 0% closed) - _is_calibrated = True still set before async_write_ha_state (REVIEW-05) - Update info log to report actual final_position (no more hardcoded 0%) - options_flow_calibration.py: legacy sender now passes explicit 0 as 4th positional arg (REVIEW-1 hygiene; behavior unchanged)
…ock, SC#4 - test_calibration_completed_timed_ends_100pct: 4-arg call with 100, asserts 100% - test_calibration_completed_legacy_ends_0pct: explicit 4-arg 0, asserts 0% - test_set_position_noop_until_calibrated: D-05 gate, no control_blind - test_set_position_unlocked_after_calibration_drives_to_midpoint: CTRL-03 / SC#3 midpoint stop via backdated _move_start_time, asserts CMD_UP + CMD_STOP - test_timed_calibration_survives_restart: SC#4 / D-13 Store round-trip and _is_calibrated=True on cover built from persisted data - Also imports _get_cal_store and _save_calibration for the restart test
Fix stale code from edit insertion point that caused NameError in test_timed_calibration_survives_restart; all 54 test_cover.py tests pass: - test_calibration_completed_timed_ends_100pct: position 100 / is_closed False - test_calibration_completed_legacy_ends_0pct: position 0 / is_closed True - test_set_position_noop_until_calibrated: uncalibrated D-05 gate holds - test_set_position_unlocked_after_calibration_drives_to_midpoint: CMD_UP then CMD_STOP at position 50 (CTRL-03 / SC#3) - test_timed_calibration_survives_restart: Store round-trip + _is_calibrated True on cover built from persisted data (SC#4 / D-13)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 4: Timed Calibration — Status: Verified ✓ (manifest →
v1.3.0)Goal: Let a user calibrate a non-bidirectional ("timed") roller-shutter motor — one that never reports movement back — by timing its close and open runs with button presses, then drive it to any position. This is the final phase of milestone v1.0.
Adds an event-free calibration flow (
TimedCalibrationFlowHandler): the legacy flow waits on motor events and hangs for timed motors — this flow blocks only on Home Assistant form rendering, never on a protocol message, so it cannot hang by construction. It times the close run (CMD_DOWN) then the open run (CMD_UP) usingtime.monotonic(), never sends a stop command on the end-press (the motor coasts to its physical endstop), guards runs to a 2–120 s window, and emitsSIGNAL_CALIBRATION_COMPLETEDwithfinal_position=100. The cover handler is made end-state aware so the timed flow lands at 100 % open while the legacy bidirectional flow still ends at 0 % closed (no regression), and the previously-deferred CTRL-03 drive-to-percentage is unlocked once real calibration times exist.Changes
Plan 04-01 — Event-free timed calibration flow
options_flow_timed_calibration.py—TimedCalibrationFlowHandler(precondition → close → open → confirm steps; 2 s/120 s guards that re-show without re-driving; redo path;final_position=100signal). Noasyncio.Event/wait_for; noCMD_STOP;time.monotonic()throughout.const.py—CAL_MIN_TRAVEL_TIME=2,CAL_MAX_TRAVEL_TIME=120.config_flow.py— reconfigure routes timed motors into the new flow (bidirectional still routes to the legacy event-based flow — CTRL-05 unchanged) + 4 delegate steps.strings.json+translations/{en,de,es,fr}.json— 4 step keys + 2 error keys (with repositioning guidance), plusdevice_not_found/timed_cal_incompleteabort strings.tests/test_timed_calibration_flow.py(11 tests) +tests/test_timed_cal_handler_structure.py.Plan 04-02 — Cover end-state awareness + CTRL-03 unlock
cover.py—_handle_calibration_completedgainsfinal_position: int = 0; end-state driven by it (100 % timed / 0 % legacy via the default arg — backward compatible with the 3-arg legacy sender).options_flow_calibration.py— legacy sender passes an explicit0(hygiene).tests/test_cover.py— D-14 end-states, CTRL-03 set-position unlock + drive-to-50 % midpoint stop, SC#4 restart survival.Requirements Addressed
Verification
device_not_foundtranslation; misleading abort reason → newtimed_cal_incomplete).threats_open: 0(5 threats, all closed/accepted).Key Decisions
asyncio.Event/wait_for) — the fix for the core "flow hangs on timed motors" bug (SC#1).CMD_STOPis ever sent; the motor coasts to its physical endstop (D-06). Guard re-shows do not re-drive (motor at unknown position).time.monotonic()throughout (nottime.time()) — immune to NTP jumps mid-run (D-07).final_positiondefault0keeps the 3-arg legacy bidirectional sender working unchanged (D-14 / CTRL-05 safe).🤖 Generated with Claude Code