Skip to content

Adaptive Cover Pro ⛅ v2.22.0

Choose a tag to compare

@jrhubott jrhubott released this 22 May 15:10
· 142 commits to main since this release

ℹ Using release notes from: release_notes/v2.22.0.md

v2.22.0

🎯 Highlights

v2.22.0 closes a seven-beta cycle that delivered two user-facing features, a large internal refactor, and a concentrated run of venetian-blind reliability fixes. The headline additions are pipeline-aware "My Position" entities — a button and a runtime-configurable number that expose the Somfy "My" preset through the managed cover with full pipeline preemption — and custom sunrise/sunset time entity options, so integrations like Sun2 can supply seasonally-adjusted window boundaries that the fixed-offset sliders cannot match. Venetian users on KNX, Shelly 2PM, FGR223, and Somfy IO hardware should see the end of false manual-override events: issue #33 received three cooperating fixes across betas 2–5 that tighten the three-tier suppression model to handle fast-settle, slow-start, and slow-bus republish scenarios. Two pipeline correctness fixes round out the release: cloud suppression no longer fires when the sun is outside the window's field of view, and user commands that already satisfy a min-mode custom-position floor are no longer preempted.


✨ Features

  • Pipeline-aware My Position button and number (#409, PRs #410, #413):

    Two new entities appear on each configured managed cover:

    button.<managed_cover>_my_position ("Managed My Position"): Pressing the button reads CONF_MY_POSITION_VALUE from config options and calls coordinator.async_apply_user_position with trigger="my_position_recall" and force=False. The pipeline preemption path runs first: if a higher-priority handler is active — force_override (priority 100), weather (90), or any custom-position slot configured above ManualOverrideHandler priority — the move is dropped and recorded as a preempted skip. When the move proceeds, min-mode floor clamping applies and mark_user_command engages manual override, so the cover holds the recalled position until the next coordinator cycle releases it.

    number.<managed_cover>_my_position_value ("Managed My Position Value", EntityCategory.CONFIG): A runtime slider (1–99 %, step 1) that exposes CONF_MY_POSITION_VALUE. Writing a new value goes through validate_options_patch and apply_options_patch, so the sunset_use_my ⇒ my_position_value cross-field validation still fires. The entity reads CONF_MY_POSITION_VALUE from config_entry.options directly so the value survives options-form saves without resetting.

    Both entities carry translation keys "my_position" (button) and "my_position_value" (number) in en.json, de.json, and fr.json. German and French translations keep "My" as a verbatim brand-name token matching the physical label on Somfy RTS remotes, rather than translating it as the possessive pronoun.

  • Custom sunrise/sunset time entities (#411, PR #415):

    Two new optional entity options — Sunset Time Entity (CONF_SUNSET_TIME_ENTITY) and Sunrise Time Entity (CONF_SUNRISE_TIME_ENTITY) — accept a sensor or input_datetime whose state is an ISO-8601 datetime string. When set, the entity value replaces the astral-computed boundary inside compute_effective_default() in helpers.py. Existing Sunset Offset / Sunrise Offset sliders remain additive on top of the entity value. The integration falls back to astral silently when the entity is unavailable, unknown, or unparseable.

    A module-level helper _read_time_entity in coordinator.py reads the entity state via get_safe_state, parses it with get_datetime_from_str, and returns a naive-local datetime or None on any failure. Typical use case: a Sun2 "sunset at −4° elevation" sensor delivers a seasonally-adjusted boundary that the static astral value cannot match.

    Translations updated for DE and FR.


🐛 Bug Fixes

Venetian blinds (issue #33)

Three cooperating fixes address false manual-override on venetian covers across different actuator hardware:

  • Post-settle cap grace — position axis (#33, beta.2): DualAxisSequencer.is_in_suppression_with_cap holds the cap bypass active for 5 seconds after a position command stamp, preventing KNX, Shelly 2PM, FGR223, and similar actuators from tripping false manual-override when cover.state settles before the tilt-walk burst finishes publishing.

  • Tilt-axis command-grace suppression (#33, PR #406): After the position-axis fix, the tilt axis still lacked a command-grace gate. VenetianPolicy.secondary_axis_check now builds a suppression callback that OR-composes the existing back-rotate window (is_in_tilt_suppression) with a new _is_in_tilt_command_grace helper that delegates to GracePeriodManager.is_in_command_grace_period. Motor drift and verification-poll republishes inside the command-grace window no longer reach the manual-override path.

  • Publish-lag window anchored to settle transition + startup grace (#33, PR #408): Two cooperating defects on slow-start actuators like Somfy IO:

    • Premature stall declaration: _wait_for_position_settle was counting consecutive unchanged-position samples as a stall before the motor had started moving (Somfy IO takes 3–5 s to begin physical travel). The fix adds VENETIAN_POSITION_SETTLE_STARTUP_GRACE_SECONDS = 6.0: stall declaration is blocked until the cover has been observed in a moving state at least once, or the startup-grace window has elapsed.

    • Wrong clock for publish-lag: is_in_suppression_with_cap was starting its 5 s cap-grace clock from stamp_position_command. On Somfy IO, the premature settle stamp started that clock 22 s early, so the real back-rotate burst arrived after the cap had already reasserted. The fix adds a third tier — VENETIAN_BACKROTATE_PUBLISH_LAG_SECONDS = 45.0 — anchored to the actual moving → settled transition recorded by _stamp_settled. The resulting three-tier suppression model: (a) carriage mid-travel: any delta suppressed; (b) command-grace tail (5 s after stamp): large deltas suppressed; (c) publish-lag window (45 s after settle): large deltas suppressed. Outside all three tiers, the geometry-bounded cap (VENETIAN_BACKROTATE_MAX_DELTA_PERCENT = 30%) applies normally.

Pipeline correctness

  • Cloud suppression skips when sun is outside window FOV (#417, PR #420): CloudSuppressionHandler was evaluating its cloud-trigger conditions regardless of direct_sun_valid. When the sun was geometrically outside the window's field of view, the handler could still win and send the cover to cloudy_position or default_position. The fix adds a direct_sun_valid guard at the top of the handler; when it is false the handler returns None and the reason string reads "cloud suppression skipped (sun outside window FOV)".

  • User commands above custom-position floor pass through (#416, PR #419): In async_apply_user_position, the pipeline was rebuilt without conveying the user's requested target. A CustomPositionHandler at higher priority than ManualOverrideHandler would win and preempt any user move — even requests that already satisfied the min-mode floor. The fix adds _winner_is_satisfied_custom_floor in coordinator.py: when the winning handler is a min-mode CustomPositionHandler and the clamped request is at or above the configured floor, the coordinator falls through to dispatch the user command. Min-mode now means what the UI describes: a floor that allows higher positions, not a forced-exact target.

  • Auto-control re-enable dispatches fresh solar position (#352, PR #414): Setting the automatic-control switch OFF→ON no longer sends the stale prior-cycle position before the pipeline runs. The coordinator now sets state_change = True and lets async_refresh route dispatch through the normal state-change path, so the first position after re-enable reflects current sun geometry.

  • "My Position Value" number survived options saves (#409, PR #413): AdaptiveCoverMyPositionNumber had async_set_native_value (the write path) but no native_value property. HA's base class returned _attr_native_value = None, so every config-entry reload (triggered by options-form saves) reset the displayed value to unknown. Fixed by adding a native_value property that reads CONF_MY_POSITION_VALUE directly from config_entry.options.

Config flow

  • "Is Sunny" sensor can be cleared in the options flow (#377, PR #393): After setting a cloud-suppression "Is Sunny" binary sensor, a blank submission silently kept the prior value. Root cause: CONF_BINARY_SENSOR was absent from the optional-entities list the schema layer uses to recognise cleared fields. Adding it makes blank submissions remove the value as expected.

  • MODE2 tilt climate handler: sun-side awareness + min_position warning (#373, PR #405): The climate handler's tilt_with_presence and tilt_without_presence methods computed tilt percentages using MODE1-only formulas that ignored the sign of the sun's azimuth offset (gamma). In MODE2, picking the wrong hemisphere produces a position on the open side rather than the blocking side. The fix adds TiltPolicy.climate_tilt_percentage as a single conversion helper that picks the correct hemisphere from the sign of gamma and routes all four call sites through it. Separately, the config-flow summary now surfaces a ⚠️ warning when MODE2 is active and min_position ≥ 50 — a configuration that silently collapses every below-centre climate result to the open-slat position.


🧹 Internal / Refactor

  • Nine-phase coordinator refactor (Phases A–I, betas 1–3): Coordinator responsibility distributed into managers/, state/, pipeline/, and cover_types/. The coordinator is ~640 lines lighter vs v2.21.5. Manual-override detection moved into managers/cover_command/state_classifier.py (preserving all five prior issue-specific fixes: #147, #172, #186, #271, #285). Cover-type-specific behaviour consolidated onto CoverTypePolicy subclasses; coordinator and pipeline handlers no longer branch on cover-type strings or hardcoded capability keys. DualAxisSequencer relocated to cover_types/venetian/ alongside VenetianPolicy. StubSingleAxisPolicy and StubDualAxisPolicy added as canary types with ~60 regression tests parametrised across all real and stub policies. Per-type label and wiki-URL mappings consolidated onto CoverTypePolicy.wiki_anchor() and display_label() hooks. Net diff for the refactor: 62 files changed, +3,682 / −1,251 lines.

  • Config-flow plumbing refactor (Phase I, beta.3, PRs #399, #400): Duplicated schema reads, step routing, and options-step boilerplate collapsed into shared helpers, reducing config_flow.py by 168 lines. Dead async_step_climate / CLIMATE_SCHEMA code removed; optional-key constants extracted adjacent to their schemas to prevent future #377-class drift.


🧪 Tests

  • ✅ 3449 tests passing (up from 3239 in v2.21.5).

Compatibility

  • Home Assistant 2026.3.0+
  • Python 3.11+
  • No breaking changes

References