Skip to content

Phase 2: Manual Device Entry#2

Merged
hrabbach merged 8 commits into
mainfrom
pr/phase-02-manual-device-entry
Jun 25, 2026
Merged

Phase 2: Manual Device Entry#2
hrabbach merged 8 commits into
mainfrom
pr/phase-02-manual-device-entry

Conversation

@hrabbach

Copy link
Copy Markdown
Owner

Summary

Phase 2: Manual Device Entry
Goal: Users can add an already-paired Schellenberg motor to Home Assistant by entering its known 2-char hex transmit address, selecting bidirectional or timed mode, and giving it a name — no auto-pairing, no calibration.
Status: Verified ✓ (automated)

Adds a "manual add" branch to the blind subentry flow: a method menu (auto-pair vs manual), a validated hex-address form with timed/bidirectional mode selection, a conditional initial-position step for timed motors, the chosen mode persisted in subentry.data, and the mode surfaced as a cover mode attribute. This also lands the bidirectional flag that Phase 3 (timed motor control) depends on.

Changes

Manual-add slice (config_flow.py, const.py, cover.py, strings.json, translations/en.json)

  • New async_step_menuasync_step_manual_add → (timed) async_step_manual_position flow; async_step_blind now opens the menu, auto-pair (async_step_user) stays a branch.
  • CONF_BIDIRECTIONAL / CONF_INITIAL_POSITION constants; mode stored as a real bool in subentry.data.
  • Hex validation (re.match(r"^[0-9A-Fa-f]{2}$")), case-normalized duplicate detection, friendly-name fallback.
  • SchellenbergCover reads the mode (missing-key default True = legacy-safe bidirectional), exposes extra_state_attributes = {"mode": ...} (excluded from recorder via _unrecorded_attributes), and seeds the initial position inside the RestoreEntity fallback (RestoreEntity wins).
  • async_step_reconfigure aborts timed motors with timed_calibration_unavailable instead of hanging in the event-waiting calibration handler.
  • Initiate button relabeled "Pair device" → "Add device"; manual-add form defaults to bidirectional (field-common case).

Code-review hardening

  • Position-loop fix: the target-reached branch now clears is_opening/is_closing/is_closed/_target_position — previously a timed motor stayed "moving" forever after a set_position and poisoned the next move.
  • Guard against an empty-device_id subentry; removed dead state and an orphaned translation key; Awaitable from collections.abc.

Tests (tests/test_config_flow.py, tests/test_cover.py)

  • 19 new tests covering the menu, validation/dedup, mode persistence, conditional position step, legacy read-default, timed-reconfigure abort, and a real position-loop regression test. Full suite: 163 passed.

Requirements Addressed

SETUP-01, SETUP-02, SETUP-03, SETUP-04, SETUP-05, SETUP-06.

Verification

  • Automated: 163 passed (WSL pytest), ruff + mypy clean on changed modules.
  • Code review (deep): 1 blocker + 6 warnings + 4 info — all resolved.
  • Nyquist validation: 8/8 automated requirements covered.
  • Security: threats_open: 0 (5 mitigated + 1 accepted, no network surface).
  • Hardware/live-HA render check — performed post-deploy (manual add → cover entity appears with mode attribute).

Key Decisions

  • Missing CONF_BIDIRECTIONAL reads as True (legacy auto-paired motors are bidirectional) — prevents a Phase-3 control regression; distinct from the manual-add form default.
  • Bidirectional manual-adds store a 2-char device_id (entry-only scope) — accepted, documented limitation; inbound 6-char frame matching is a future story.
  • Timed-motor calibration is deferred to Phase 4; timed reconfigure aborts cleanly until then.

hrabbach and others added 8 commits June 25, 2026 22:47
- Add tests/test_config_flow.py with 8 test functions:
  test_manual_add_menu_shown, test_manual_add_creates_subentry,
  test_manual_add_mode_flag, test_manual_add_invalid_enum,
  test_manual_add_duplicate_enum, test_manual_add_device_name,
  test_manual_add_position_step_timed_only,
  test_reconfigure_timed_motor_aborts
- Add 3 cover tests to tests/test_cover.py:
  test_cover_mode_attribute, test_cover_mode_defaults_bidirectional_when_key_absent,
  test_cover_initial_position_from_subentry
- All tests RED: fail on missing CONF_BIDIRECTIONAL/CONF_INITIAL_POSITION
  symbols and missing flow methods (expected RED state)
…, cover reads

- const.py: add CONF_BIDIRECTIONAL ('bidirectional') and CONF_INITIAL_POSITION
- config_flow.py: async_step_blind now delegates to async_step_menu; add
  async_step_menu (shows 'Pair automatically' / 'Add manually' menu),
  async_step_manual_add (validates 2-hex enum, dedup check, mode, name),
  async_step_manual_position (initial position slider, timed only); extend
  __init__ with _pending_is_bidirectional/_pending_initial_position; add
  timed-motor guard at top of async_step_reconfigure (REVIEW-2)
- cover.py: add CONF_BIDIRECTIONAL/CONF_INITIAL_POSITION imports; read both
  in __init__ (CONF_BIDIRECTIONAL default True for legacy subentries, REVIEW-1);
  add extra_state_attributes returning mode; seed initial position in
  async_added_to_hass (RestoreEntity takes precedence)
- strings.json + translations/en.json: relabel initiate_flow.user to 'Add device'
  (REVIEW-3); add menu/manual_add/manual_position steps, invalid_enum_format +
  duplicate_enum errors, timed_calibration_unavailable abort
- tests/test_config_flow.py: fix handler binding (_make_handler helper with tuple
  handler + context source=user); all 11 Phase-2 tests now GREEN (34 pass)
…g, full gate

- tests/test_config_flow.py: add test_manual_add_enum_case_normalized (lowercase
  '1a' collides with existing '1A' after .upper()), test_manual_add_enum_stored_uppercase
  ('2b' stored as '2B')
- tests/test_cover.py: add test_cover_initial_position_clamped (CONF_INITIAL_POSITION=150
  clamps to 100; restored prior state of 50 beats seeded initial of 100)
- Full WSL pytest suite: 160 passed, 0 failures (no regressions)
- Native ruff: All checks passed; mypy: Success, no issues in 3 source files
… (CR-01)

In _async_position_update_loop, the position_reached branch now clears
_attr_is_opening, _attr_is_closing, _attr_is_closed, and _target_position
before returning -- matching the two boundary-exit branches that already
did this correctly. Without this fix, timed motors were left stuck in
a phantom opening/closing state indefinitely after SET_POSITION completed.

Also adds _unrecorded_attributes = frozenset({'mode'}) to SchellenbergCover
so the static mode attribute is excluded from the recorder (WR-05).
…ion, and cleanups (WR-01/03/04, IN-01/02)

- WR-01: async_step_manual_position now aborts with pairing_failed if
  _pending_device_enum is missing, preventing a broken empty-id subentry.
- WR-03: BooleanSelector default and resolver default for CONF_BIDIRECTIONAL
  flipped from False to True so an unmodified form yields bidirectional,
  matching the field-common case and the read-default used everywhere else.
- WR-04: removed the never-used _pending_initial_position attribute from __init__.
- IN-01: Awaitable moved from typing to collections.abc (deprecated in 3.9+).
- IN-02: Unicode en-dash in comment replaced with plain ASCII hyphen.
… orphaned menu key (WR-02, IN-04)

- WR-02: manual_position step description now notes that positioning for
  timed motors uses a default travel time and is uncalibrated until Phase 4.
- IN-04: removed the orphaned config.step.menu.menu_options.pair_device_menu
  key from both strings.json and translations/en.json; no matching flow step
  exists. Subentry-level user/manual_add keys are unaffected.
…and WR-03 default (WR-06)

- test_timed_motor_position_loop_clears_flags: exercises the REAL
  _async_position_update_loop for a timed motor with tiny travel time,
  drives SET_POSITION to 50, and asserts is_opening=False, is_closing=False,
  _target_position=None, and position==50 after completion (WR-06/CR-01).
- test_manual_position_aborts_without_pending_state: asserts abort with
  pairing_failed when manual_position is entered without prior manual_add (WR-01).
- test_manual_add_default_mode_is_bidirectional: asserts that omitting
  CONF_BIDIRECTIONAL from the form yields CONF_BIDIRECTIONAL=True (WR-03).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QwPMgtiypLgJ5nkR3mUGfL
@hrabbach hrabbach merged commit c28e96a into main Jun 25, 2026
1 check passed
@hrabbach hrabbach deleted the pr/phase-02-manual-device-entry branch June 25, 2026 21:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant