Skip to content

feat: UI-first architecture with config flow, card, calibration, tilt strategies, and state monitoring#49

Closed
clintongormley wants to merge 158 commits intoSese-Schneider:mainfrom
clintongormley:feat/ui-sync-rewrite
Closed

feat: UI-first architecture with config flow, card, calibration, tilt strategies, and state monitoring#49
clintongormley wants to merge 158 commits intoSese-Schneider:mainfrom
clintongormley:feat/ui-sync-rewrite

Conversation

@clintongormley
Copy link
Contributor

Summary

Major rewrite that transforms Cover Time Based from a YAML-only integration into a modern UI-first Home Assistant helper with comprehensive configuration, calibration, and monitoring capabilities.

  • Config flow & UI setup: One-step config flow creates cover helpers via the UI; YAML deprecated with repair issue
  • Lovelace configuration card: Full card for configuring device type, entities, timing, tilt, calibration, and position reset — all via WebSocket API
  • Calibration system: Services and UI for measuring travel time, tilt time, motor startup delay, and minimum movement time with automated step tests
  • Tilt strategies: Three pluggable tilt modes — inline (synchronized), sequential (close-then-tilt), and dual_motor (separate tilt motor with safety guards)
  • Input mode subclasses: Extracted CoverTimeBased base class with SwitchModeCover, PulseModeCover, ToggleModeCover, and WrappedCoverTimeBased subclasses
  • External state monitoring: Detects physical switch presses and keeps position tracker in sync, with echo filtering and per-mode tilt handling
  • Translatable frontend: All card strings externalized to strings.json with translations for EN, PT, PL
  • Local TravelCalculator: Replaced external xknx dependency with a local HA-convention copy (zero external dependencies)
  • 97% test coverage: 478 tests across 19 test files, pyright clean, ruff clean

Test plan

  • python -m pytest tests/ -v — 478 tests passing
  • python -m pytest tests/ --cov=custom_components/cover_time_based --cov-report=term-missing — 97% coverage
  • ruff check . && ruff format --check . — clean
  • pyright — 0 errors
  • Manual testing with physical covers (switch, pulse, toggle modes)
  • Manual testing of Lovelace card configuration flow
  • Manual testing of calibration workflows

🤖 Generated with Claude Code

clintongormley and others added 30 commits February 16, 2026 12:10
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds the main integration entry point that handles config entry setup,
unload, and options updates. Forwards setup to the cover platform.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ry flows

- Add config flow strings for setup wizard
- Add options flow strings for default timing settings
- Add config subentries for cover management with add/reconfigure flows
- Add selector translations for device type and input mode
- Include translations for travel timing and advanced settings sections
- Preserve existing service translations for set_known_position and set_known_tilt_position

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implement three flow handlers for UI-based configuration:
- CoverTimeBasedConfigFlow: creates the integration entry
- CoverTimeBasedOptionsFlow: edits integration-level default timing
- CoverTimeBasedSubentryFlow: adds/reconfigures individual cover entities

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add three functions to support creating cover entities from config
entry subentries alongside the existing YAML-based async_setup_platform:

- async_setup_entry: iterates subentries to create entities and
  registers custom services
- _get_subentry_value: resolves config values with priority from
  subentry data, entry options, then schema defaults
- _entity_from_subentry: constructs a CoverTimeBased entity from
  a config subentry, handling both switch and cover device types

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drop the subentry/defaults pattern in favor of a simpler approach:
each cover is its own config entry with all settings in one form.

- Single page with name, entities, input mode, timing, and a
  collapsible advanced section
- Options flow for reconfiguration with same form
- Human-friendly labels and descriptions
- No shared defaults — each cover is self-contained

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Step 1: device type (radio) + input mode (radio)
- Step 2: entity fields shown conditionally based on device type,
  pulse time only shown for pulse/toggle modes
- Remove name field (handled by helper framework)
- Same two-step flow for options/reconfigure

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add Name field to step 1, move tilt fields into collapsible "Tilt settings"
section, move pulse_time to Advanced section, add full translations to
translations/en.json, and move description below the name field.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add deprecation warning log and repair issue when covers are configured
via YAML, directing users to recreate via the UI helpers page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Use Platform enum in __init__.py
- Add type annotations to async_setup_entry (HomeAssistant, ConfigEntry, AddEntitiesCallback)
- Move CONF_DEVICE_TYPE/DEVICE_TYPE_* constants to cover.py to avoid magic strings
- Remove misleading `adv = d` alias in _build_details_schema
- Add tilt time pair validation (both or neither must be set)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Set up pytest test infrastructure with shared fixtures (make_hass, make_cover)
and write characterization tests that document the exact relay command sequences
for each input mode (switch, pulse, toggle, wrapped cover). Also add toggle
behavior tests covering same-direction stop, idle stop guard, and direction
change scenarios. These tests serve as a safety net before refactoring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move CoverTimeBased from cover.py to cover_base.py and add abstract
_send_open, _send_close, _send_stop methods. Refactor _async_handle_command
to dispatch to these abstract methods while keeping state setting and
async_write_ha_state in the dispatcher. Update tests to use a concrete
CoverTimeBasedTest subclass that implements the original relay logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create five subclasses of CoverTimeBased, each implementing the
_send_open/_send_close/_send_stop abstract methods for a specific
input mode:

- WrappedCoverTimeBased: delegates to cover.open/close/stop_cover
- SwitchCoverTimeBased: abstract mid-level base for switch-controlled covers
- SwitchModeCover: latching relay mode (switches stay on during movement)
- PulseModeCover: momentary pulse mode (brief pulse then switch off)
- ToggleModeCover: toggle mode (re-press direction to stop, overrides
  async_close/open/stop_cover for same-direction-stops-movement behavior)

All 54 tests pass (25 original + 29 new subclass tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…guards from base

- Add _create_cover_from_options() factory in cover.py that creates the
  correct subclass (WrappedCoverTimeBased, SwitchModeCover, PulseModeCover,
  ToggleModeCover) based on options dict
- Update async_setup_entry and devices_from_config to use the factory
- Clean up CoverTimeBased base class: remove device-specific params
  (switch entity IDs, input_mode, pulse_time, cover_entity_id) from
  constructor - each subclass now only accepts what it needs
- Remove toggle-specific guards from base class (async_close_cover,
  async_open_cover, async_stop_cover, set_known_position,
  set_known_tilt_position) - toggle behavior now lives in ToggleModeCover
- Add stop-before-direction-change to base class async_close_cover and
  async_open_cover (benefits all modes, not just toggle)
- Move CONF_* constants to cover_base.py to eliminate deferred import
  in extra_state_attributes
- Add -> None return type annotations to all _send_open/_send_close/
  _send_stop implementations
- Update all test factories to use new constructor signatures
- Remove CoverTimeBasedTest from conftest; make_cover now uses the
  factory to create real subclasses

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t_position

Use _async_handle_command(SERVICE_STOP_COVER) instead of bare _send_stop()
to ensure self._state is set and async_write_ha_state() is called.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract _start_movement() helper to replace 6 duplicated startup-delay patterns
- Unify async_close_cover/async_open_cover into _async_move_to_endpoint(target)
- Unify async_close_cover_tilt/async_open_cover_tilt into _async_move_tilt_to_endpoint(target)
- Extract _handle_pre_movement_checks() and _is_movement_too_short() from set_position/set_tilt_position
- Fix startup delay conflict check ordering: check before position check so direction reversal during startup delay is properly cancelled

Reduces cover_base.py from 1057 to 767 lines (27% reduction).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
63 new tests covering:
- Travel/tilt endpoint movement (close, open, direction changes)
- Tilt-travel coupling (both directions, with/without coupling)
- set_position and set_tilt_position (direction, already-at-target, endpoints)
- Min movement time filtering (short moves, endpoints always allowed)
- Startup delay (creation, same-direction dedup, direction-change cancellation)
- Relay delay at endpoints (auto-stop, mid-point stop, cancellation)
- Stop cover, set_known_position/tilt, properties, tilt constraints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Extract _calc_coupled_target() and _begin_movement() in cover_base.py
  to eliminate repeated coupled-calculator and start-movement patterns
  across _async_move_to_endpoint, _async_move_tilt_to_endpoint,
  set_position, and set_tilt_position
- Simplify devices_from_config() in cover.py with data-driven loop over
  _TIMING_DEFAULTS, extract _get_value() and _resolve_input_mode()
- Apply ruff formatting fixes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes "Task was destroyed but it is pending" warnings in CI by
cancelling any leftover _startup_delay_task when tests finish.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pytest-homeassistant-custom-component verify_cleanup fixture
detects pending tasks before our make_cover teardown runs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t_motor_overhead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
clintongormley and others added 29 commits February 21, 2026 11:27
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add abstract `restores_tilt` property to the TiltStrategy base class.
SequentialTilt returns False (no tilt restore after position changes),
DualMotorTilt returns True (restores tilt via 3-phase lifecycle).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…onstraints

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove xknx dependency by copying TravelCalculator with native HA
position convention (0=closed, 100=open). Remove proportional tilt
strategy (redundant). Fix position convention in toggle and websocket
tests, update calibration to exclude proportional references.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Inline: snap_trackers_to_physical is now a no-op — tilt can be any
value at any position. Endpoint coupling during travel is handled by
plan_move_position pre-steps, not post-stop snapping. This fixes
tilt jumping back to 0 after an explicit tilt command at closed.

Dual motor: when traveling to an endpoint (0 or 100), tilt now snaps
to the endpoint value after travel completes instead of restoring to
the pre-movement tilt position.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add inline to hasIndependentTilt and hasTiltCalibration checks so
tilt timing and position reset dropdowns appear for inline tilt mode.
Add inline-specific calibration hint text.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New position or tilt commands now cancel any in-progress multi-phase
tilt lifecycle (pre-step, restore) before starting the new movement.
Prevents stale pending travel from executing after the user changes
their mind mid-operation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Split CALIBRATION_OVERHEAD_STEPS into separate constants for travel (8)
and tilt (3) since tilt has a much shorter travel range.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… registration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…change handler

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive tests for previously uncovered areas:
- cover_base.py: properties, state restoration, auto_updater, echo
  filtering, external state changes, delayed_stop, tilt constraints
- cover.py: factory function, YAML config parsing, platform setup
- cover_switch.py/cover_switch_mode.py: external state change handlers
- cover_toggle_mode.py: _send_close with stop switch
- config_flow.py: full config/options flow, helpers, validation
- __init__.py: integration setup/unload/options update lifecycle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add _handle_external_state_change to WrappedCoverTimeBased that detects
opening/closing state transitions on the wrapped cover entity and starts
position tracking. Echo filtering via _mark_switch_pending prevents
double-processing self-initiated state changes.

Also adds tests for all modes: wrapped cover, toggle close-while-closing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test imported CoverTimeBasedOptionsFlow, _build_details_schema,
_flatten_input, etc. which don't exist in config_flow.py yet.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add complete external state change handling for all cover modes:

- Toggle mode: react to both ON→OFF and OFF→ON with debounce, same-direction
  stops, external cross-direction stops (motor already stopped physically)
- Switch mode: latching tilt handler (ON=start tracking, OFF=stop tracking)
- Pulse mode: stop switch handling, tilt switch echo filtering (pending=2
  for direction switches to handle ON+OFF transitions)
- Wrapped cover: handle direction changes (opening→closing) not just
  start/stop transitions

Base class changes:
- Register tilt switch listeners (_tilt_open/close/stop_switch_id)
- Route tilt switch events through _handle_external_tilt_state_change
- Add echo filtering (_mark_switch_pending) to _send_tilt_open/close/stop
- Guard tilt relay commands with _triggered_externally flag
- Fix safe_tilt_position factory default (use `or 100` for falsy values)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite calibration hint descriptions for sequential, dual_motor, and
  inline tilt modes with clearer starting positions
- Default safe_tilt_position to 100 (fully open) when switching to dual_motor
- Deduplicate hint text by referencing earlier hint variables

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add "card" section to strings.json with all 84 translatable strings
- Sync en.json with strings.json
- Add _t(key) helper with HA WebSocket translation loading and EN fallbacks
- Replace all hardcoded strings in cover-time-based-card.js with _t() calls
- Add Portuguese (pt) and Polish (pl) translations for card section

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Document all three tilt modes (inline, sequential, dual_motor)
- Add external state monitoring section with per-mode behavior
- Document dual_motor config options (safe position, max allowed)
- Add unreleased changelog section with all recent features and fixes
- Update maintenance year badge to 2026

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove config entry v1→v2 migration (config flow is new, no v1 entries exist)
- Reset config flow VERSION from 2 to 1
- Remove CONF_TRAVEL_MOTOR_OVERHEAD / CONF_TILT_MOTOR_OVERHEAD (only existed on this branch)
- Remove "proportional" tilt mode compat shim (only existed on this branch)
- Remove dead calc_coupled_target() (never called)
- Remove dead _save_calibration_result() (saving done via WebSocket API)
- Remove dead get_calibratable_attributes() (frontend does its own filtering)
- Move DEFAULT_TRAVEL_TIME to tests (not used in production)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add pyright config to suppress HA-inherent issues (base class conflicts,
private import warnings). Fix actual type errors: add None guards for
calibration state access, tilt restore target, config entry resolution,
and possibly-unbound command variable.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 3 new test files and extend 7 existing ones to cover previously
untested code paths across config flow, travel calculator, service
handlers, websocket API, calibration edge cases, tilt state monitoring
(pulse/switch/toggle modes), and unknown-position movement.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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