Skip to content

Add integration tests and fix 6 behavioral bugs#52

Closed
clintongormley wants to merge 58 commits intoSese-Schneider:mainfrom
clintongormley:feat/integration-tests
Closed

Add integration tests and fix 6 behavioral bugs#52
clintongormley wants to merge 58 commits intoSese-Schneider:mainfrom
clintongormley:feat/integration-tests

Conversation

@clintongormley
Copy link
Contributor

Summary

  • Fix 6 bugs identified during behavioral spec review
  • Add integration test infrastructure using pytest-homeassistant-custom-component
  • Add 13 focused integration test scenarios covering movement lifecycle, switch feedback loop, tilt lifecycle, mode-specific behavior, and config lifecycle

Bug Fixes

  1. Remove stop switch support from switch/toggle modes (pulse-only)
  2. Pulse mode reacts on rising edge (OFF->ON), not falling edge
  3. Toggle mode reacts on OFF->ON only, ignores ON->OFF
  4. Remove toggle mode same-direction override
  5. Endpoint resync sends command + runon when already at target
  6. Tilt calibration uses 1/5 steps, not 1/10

Integration Tests

  • Movement: open/track/auto-stop, stop-during-movement, set_position, endpoint resync
  • Feedback: echo filtering, external button press detection
  • Tilt: sequential tilt-before-travel pre-step, tilt rejection at non-endpoints
  • Modes: toggle stop-before-reverse, pulse relay pulsing
  • Lifecycle: correct entity features from config, position restore on restart
  • Smoke: config entry loads and creates entity

Test plan

  • All 675 existing unit tests pass
  • All 13 new integration tests pass (688 total)
  • ruff check + format clean
  • pyright 0 errors
  • Manual test: switch mode open/close/stop
  • Manual test: pulse mode with stop switch
  • Manual test: toggle mode direction change
  • Manual test: endpoint resync (press close when already closed)

🤖 Generated with Claude Code

clintongormley and others added 30 commits February 21, 2026 22:26
- Add pyproject.toml with pytest, ruff, and pyright configuration
- Add GitHub Actions workflow for tests and linting
- Update .gitignore with .coverage and .iml entries

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract the core cover logic from the monolithic cover.py into
cover_base.py (CoverTimeBased base class) with abstract methods for
relay control. Add concrete subclasses for each input mode:
- cover_switch_mode.py: separate open/close switches
- cover_toggle_mode.py: single toggle switch with directional state
- cover_pulse_mode.py: momentary pulse switch control
- cover_wrapped.py: wraps an existing HA cover entity
- cover_switch.py: helper for switch entity operations

Slim cover.py to a thin factory that creates the appropriate subclass
based on the input_mode configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a standalone travel_calculator.py that follows HA conventions,
removing the xknx library dependency. Handles position tracking,
movement timing, and travel state management.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace tilt mode strings with a strategy pattern. Each strategy
encapsulates movement planning and command generation:
- base.py: TiltStrategy ABC with MovementStep dataclasses
- sequential.py: SequentialTilt with phased movement model
- dual_motor.py: DualMotorTilt with boundary lock and safety
- inline.py: InlineTilt using the main motor for tilt control

Factory function creates the appropriate strategy from config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add __init__.py with async_setup_entry for config entry support
- Add config_flow.py with UI-based setup wizard
- Update manifest.json with config_flow dependency and version bump

Enables UI-first setup, deprecating YAML-only configuration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add calibration.py with automated timing calibration services:
- start_calibration/stop_calibration for travel time measurement
- Motor overhead calibration with two-phase step test
- Min movement time calibration with incremental pulses
- Tilt time calibration with travel_moves_with_tilt validation
- CalibrationState dataclass for tracking active calibration

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add websocket_api.py providing real-time configuration management:
- get_config/save_config for cover timing parameters
- get_all_covers for listing configured covers with state
- start/stop_calibration commands with progress reporting
- set_known_position/set_known_tilt_position for position reset
- raw_command for direct relay control bypassing position tracker

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update services.yaml with calibration and position reset services
  using target selectors for UI mode
- Update strings.json with config flow, calibration, and service
  descriptions
- Update translations for en, pt; add pl translation
- Add translatable strings for all calibration attributes and
  frontend card labels

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add cover-time-based-card.js (1599 lines) providing a full
configuration UI:
- Device tab: entity pickers, timing table with save/discard,
  input mode and tilt mode selectors
- Calibration tab: automated calibration with progress feedback,
  position reset, and per-attribute hints
- Tilt Motor tab: strategy-specific entity pickers and config
- All strings are translatable via HA's localization system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 19 test files with ~10,000 lines of tests covering:
- Base movement orchestration and position tracking
- All input mode subclasses (pulse, switch, toggle, wrapped)
- Cover factory and service dispatching
- Calibration system with all calibration types
- WebSocket API endpoints and config management
- Tilt strategy behavior for all strategy types
- State monitoring with echo filtering
- Travel calculator position/timing logic
- Config flow and integration setup
- Relay command sequences and motor overhead

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite README for UI-first setup, deprecate YAML-only docs
- Add sections for calibration, tilt strategies, state monitoring
- Update CHANGELOG with all new features and breaking changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 20 design and plan documents covering:
- UI config flow design and implementation plan
- Input mode subclasses design and refactor plan
- State monitoring design and implementation plan
- Calibration APIs design and implementation plan
- Configuration card design and plan
- Manual position reset design and plan
- Tilt strategies design, refactor plan, and config UI plan
- Inline tilt design and implementation plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When travel or tilt position is None (e.g. newly created entity with no
restored state), None != 0 evaluates to True, causing set_position(100)
to silently force tilt to 100% on unknown state. Add the same None guard
that DualMotorTilt already has.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move CONF_*, DOMAIN, and DEFAULT_* constants that were duplicated
across cover_base.py, cover.py, websocket_api.py and __init__.py
into a single const.py module.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move _resolve_entity from cover.py and websocket_api.py into a shared
helpers.py module as resolve_entity (raises) and resolve_entity_or_none
(returns None). Update all call sites and test mock paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move 10 calibration methods (337 lines) from CoverTimeBased into
CalibrationMixin in cover_calibration.py. CoverTimeBased now inherits
from CalibrationMixin, keeping all behavior identical.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract _extract_coupled_tilt, _extract_coupled_travel, and
_calculate_pre_step_delay from CoverTimeBased into module-level
functions in tilt_strategies/planning.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract the ~45-line tilt coupling block duplicated between
_async_move_to_endpoint and set_position into _plan_tilt_for_travel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reorganize definitions across 5 files for top-to-bottom readability:
- cover_base.py: group by lifecycle/properties/public API/internals
- cover_toggle_mode.py: public overrides → state handlers → relay cmds
- dual_motor.py: move can_calibrate_tilt after __init__
- test_websocket_api.py: group unwraps at top, classes match source order
- cover.py: group constants, move _register_services after setup

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>
Pulse/toggle send methods now return immediately after the ON edge,
deferring the sleep+turn_off to a background task. This fixes calibration
overhead measuring as zero and improves position tracking accuracy.

Also adds _send_tilt_open/close/stop overrides to PulseModeCover and
ToggleModeCover so tilt buttons use the same pulse/toggle pattern as
travel buttons. Fixes ToggleModeCover.async_stop_cover missing the
_send_tilt_stop call when tilt restore/pre-step was active.

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

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

_async_handle_command dispatches to _send_open/_send_close but never set
_last_command. The higher-level methods (async_open/close_cover) set it,
but calibration calls _async_handle_command directly. Toggle mode's
_send_stop needs _last_command to know which button to re-pulse, so it
silently skipped every stop during calibration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… switch for pulse mode

Replace the two-field device_type + input_mode system with a single
control_mode field (wrapped/switch/pulse/toggle). No migration needed
as this code hasn't been released yet.

Pulse mode now requires a stop switch entity — the entity shows as
unavailable in HA and the calibration tab is disabled until configured.
Tilt stop switch is also required for pulse mode with dual_motor tilt.

Tilt entity visibility now follows control mode:
- wrapped: no tilt entity pickers
- switch/toggle: tilt open + close
- pulse: tilt open + close + stop (required)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Change min=0 to min=0.1 for travel_time_close, travel_time_open,
tilt_time_close, and tilt_time_open. Matches the YAML schema's
cv.positive_float behavior and prevents division-by-zero in the
position calculator and calibration logic.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix _async_move_tilt_to_endpoint and set_tilt_position to use tilt
  motor relays instead of main motor in dual_motor mode
- Add tilt motor stops at intermediate cleanup points for safety
- Unify _has_tilt_motor() to check tilt_strategy.uses_tilt_motor
  consistently between base and wrapped classes
- Map legacy travel_moves_with_tilt YAML config to inline tilt mode
- Fix frontend auto-save entity_id override order
- Add assertion for _start_pending_travel command narrowing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Declare host class attributes and methods under TYPE_CHECKING so pyright
can resolve them without runtime circular imports. Adds assertions for
str | None narrowing where values are guaranteed set by callers.

Resolves all 39 reportAttributeAccessIssue pyright errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
HACS rejects the non-standard "card" key in strings.json. Move all card
UI translations into the JS bundle and look up locale via hass.language.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The pytest_homeassistant_custom_component verify_cleanup fixture
detects pending _complete_pulse() tasks after tests that intentionally
skip draining. Add _cancel_tasks() cleanup to the 6 affected tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
clintongormley and others added 27 commits February 24, 2026 13:50
1. Tilt startup delay calibration used hardcoded 0.2 * total_time
   (correct for 8 travel steps but wrong for 3 tilt steps). Now
   computed dynamically: (1 - step_count/10) * total_time.

2. _start_tilt_restore cleared _last_command after dispatching the
   motor command. In toggle mode, _send_stop needs _last_command to
   know which relay to re-pulse — clearing it left the motor running.
   Removed the premature clear; auto_stop_if_necessary clears it
   after the stop is sent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When calibrating tilt_time_close from the closed_tilt_open position,
the card was sending direction="open" based on cover position alone.
This overrode the server's correct attribute-based fallback which
derives direction from the attribute name (tilt_time_close → close).

Now only sends explicit direction for travel attributes; tilt attributes
let the server determine direction from the attribute name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace `kwargs.get("safe_tilt_position") or 100` with
  `kwargs.get("safe_tilt_position", 100)` so 0 is no longer treated
  as falsy and silently replaced with 100.
- Fix README pulse_time default from 2s to 1s to match code.
- Add tests for safe_tilt_position=0 and timing field validation bounds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
set_known_position and set_known_tilt_position were calling
_async_handle_command(SERVICE_STOP_COVER), which in toggle mode
re-pulsed the last switch (starting motor movement). Now these
methods only update internal tracker state.

Also rename log labels from UP/DOWN to OPEN/CLOSE, and fix
hardcoded 0.2 * total_time in overhead calibration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents overflow with long entity names by placing the
open/stop/close buttons below the position helper text
instead of in the blue header bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Wrapped cover entities fire attribute-only state changes (e.g.
closing→closing with position updates) that bypassed echo filtering
and triggered redundant async_close_cover calls. During calibration,
these overrode the travel calculator's target position.

Two guards added to _async_switch_state_changed:
- Skip when old_val == new_val (attribute-only, no real transition)
- Skip when calibration is active (calibration drives motors directly)

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

Rewrite overhead calibration to use travel calculator position tracking
instead of time-based sleeping. Each step uses calc.start_travel() and
polls current_position(), then force-sets position after stopping.

- Zero startup delay during test so tracker doesn't compensate
- Restore saved delay when calibration ends (success or cancel)
- Use correct direction's travel time in result calculation
- Subtract pulse_time from continuous phase for pulse/toggle modes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 99 new tests covering dual-motor tilt operations, calibration
edge cases, relay echo pending logic, toggle mode tilt handling,
legacy YAML migration, and external tilt switch state changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a physical button or remote triggers the cover externally,
the tracked position is now set to Unknown (None) instead of
attempting inaccurate time-based tracking. Position stays Unknown
until set_known_position is called.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove premature validation that blocked changing to pulse mode
without a stop switch. The cover will show as unavailable until
the stop switch is configured, which is the correct UX — users
need to be able to change the mode first, then add the switch.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With max_tilt_allowed_position set, moving the cover to a position
above the boundary would incorrectly restore tilt to its pre-move
value, then immediately snap it back to safe position. Now the
restore is skipped when the target position doesn't allow tilt.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Calibration screen open/close/stop buttons now work correctly:
  - ws_raw_command stops active lifecycle before sending commands
  - New _raw_direction_command method with toggle mode override
    (stop-before-reverse for toggle motor controllers)
  - Position clears to Unknown when using calibration buttons
  - Frontend card resets _knownPosition dropdown on button press

- Physical button / wall switch state changes now tracked correctly:
  - _async_switch_state_changed delegates to mode-specific handlers
    with _triggered_externally=True (instead of clearing position)
  - External movements skip tilt planning (relay already on,
    can't sequence pre-steps for physical buttons)

- Pylint cleanup (9.80 → 9.84): class docstring, unused args,
  line-too-long, consider-using-in

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>
Behavioral spec documents expected behavior for all control modes,
tilt strategies, and lifecycle phases — reviewed and corrected by
maintainer. Integration test design describes hybrid approach:
keep unit tests for pure logic, add focused HA integration tests
for feedback loop, multi-phase lifecycle, and config wiring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
13 tasks across 4 phases: bug fixes (TDD), integration test
infrastructure, 12 integration test scenarios, final validation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The ON signal is the button press; OFF is just the release and should
be ignored. Applies to both travel and tilt external state handlers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
ON→OFF transitions are just relay releases and should be ignored.
Only the rising edge (OFF→ON) indicates a real button press.
Applies to both travel and tilt external state handlers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover services (open/close/stop) now behave identically regardless of
control mode. No special-casing where same-direction = stop in toggle
mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pressing open/close when tracker already shows the endpoint still
sends the relay command plus run-on time. This allows physical
covers that are out of sync to resync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tilt calibration now derives step size from num_steps (3 steps → 20%
each) instead of hardcoding 10%. Travel calibration (8 steps → 10%)
is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Uses pytest-homeassistant-custom-component for a real HA instance.
input_boolean entities simulate physical switches. MockConfigEntry
loads the integration through the real config entry lifecycle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests open/track/auto-stop, stop-during-movement, set_position,
and endpoint resync through real HA service calls, event bus,
and switch state changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Feedback: echo filtering and external button press detection
- Modes: toggle stop-before-reverse and pulse relay pulsing
- Lifecycle: correct entity features from config, position restore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests tilt-before-travel pre-step and tilt rejection when
cover is not at an endpoint position.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@clintongormley
Copy link
Contributor Author

Damn Claude! Closing...

@clintongormley clintongormley deleted the feat/integration-tests branch March 8, 2026 19:49
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