Skip to content

Releases: jrhubott/adaptive-cover-pro

Adaptive Cover Pro ⛅ v2.27.1-nightly.202606091034

09 Jun 10:40

Choose a tag to compare

🌙 Nightly build from develop — unstable, for testing.
Auto-generated from the latest development code. Expect rough edges — not for production use.
Hit a problem? Open an issue and include this nightly version string.

What's Changed

  • perf+fix: gate redundant writes, cache geometry, sun.sun guard by @jrhubott in #543

Full Changelog: v2.27.0...v2.27.1-nightly.202606091034

Adaptive Cover Pro ⛅ v2.27.0

09 Jun 04:08

Choose a tag to compare

ℹ Using release notes from: release_notes/v2.27.0.md
2.27.0 brings forecast/runtime parity, opt-in movement minimization for sun-tracking covers, and sun-state classification exposed through diagnostics and the decision_trace sensor. A false manual override that fired when a cover returned from unavailable is fixed. The release tooling also gains a ZIP-structure guard to prevent a double-nested HACS install path.


🎯 Highlights

  • Forecast/runtime parity (#556): the position forecast now calls the same snapshot-free primitives as the live pipeline, so min/max limits, floor-at-1%, rounding, and movement minimization all match runtime by construction.
  • Opt-in movement minimization (#555): a new CONF_MINIMIZE_MOVEMENTS option quantizes sun-tracking positions to a configurable number of coverage steps, reducing small cover movements without changing average coverage.
  • Sun-state classification (#552, #553, #554): a SunState enum (HITTING, IN_FOV_NOT_VALID, OUTSIDE_FOV) is computed per cycle, surfaced in diagnostics, and exposed as sun_state on the decision_trace sensor attributes.
  • False manual override suppressed (#546, #551): covers returning from unavailable no longer trigger a spurious manual override when no prior command target exists.

✨ Features

Forecast/runtime parity (#556)

The position forecast in forecast.py previously re-implemented a subset of the live position math independently. It never applied min/max position limits, did not floor solar samples at 1% or round them, and used the raw default height rather than the sunset position during the sunset window. The result was a forecast that visually diverged from what the live pipeline would produce.

Three snapshot-free primitives now own the shared math in pipeline/helpers.py: solar_position_from_geometry computes the sun-tracking position from geometry alone, default_position_with_limits computes the default (sunset-aware) position with min/max limits applied, and apply_config_limits applies the configured position bounds. The live pipeline helpers in pipeline/helpers.py are thin adapters over these primitives. The forecast calls the same functions, so limits, floor-at-1, rounding, and movement minimization match runtime by construction.

compute_effective_default gained an eval_time keyword argument so the forecast can compute the sunset-aware default at each sample's projected time rather than at the current moment. A parity test locks compute_solar_position and compute_default_position against future drift between the two paths.

  • forecast.py — now calls shared primitives instead of duplicating position math
  • pipeline/helpers.pysolar_position_from_geometry, default_position_with_limits, apply_config_limits extracted here

Opt-in movement minimization (#555)

Small positional corrections as the sun moves across the sky cause covers to jog frequently. The new CONF_MINIMIZE_MOVEMENTS option (default off) quantizes the computed sun-tracking position to a configurable number of evenly-spaced coverage steps via quantize_to_coverage_steps() in position_utils.py. Step count is controlled by CONF_MAX_COVERAGE_STEPS (default DEFAULT_MAX_COVERAGE_STEPS). Quantization applies after the raw solar position is computed, so it interacts correctly with min/max limits and the floor-at-1% rounding already in place.

The feature applies to tilt, blind, and awning cover types. It is surfaced in the config flow options UI, in services.yaml for service calls, and in all three translation files.

  • position_utils.pyquantize_to_coverage_steps()
  • pipeline/handlers/solar.py — applies quantization when CONF_MINIMIZE_MOVEMENTS is enabled
  • services.yamlCONF_MINIMIZE_MOVEMENTS, CONF_MAX_COVERAGE_STEPS exposed
  • translations/en.json, translations/de.json, translations/fr.json — labels and descriptions

Sun-state classification in diagnostics and decision trace (#552, #553, #554)

Each update cycle now classifies the sun's relationship to the cover window using the new SunState StrEnum in engine/sun_geometry.py, with three values: HITTING (sun is directly hitting the cover), IN_FOV_NOT_VALID (sun is within the field of view but not at a valid elevation or angle), and OUTSIDE_FOV (sun is outside the configured field of view entirely). The in_fov() method on sun geometry classes drives the classification.

diagnostics/builder.py reads the classification from the pipeline result and includes it in the diagnostics payload, making it visible in the HA diagnostics download. sensor.py surfaces sun_state as an attribute on the decision_trace sensor, so dashboards and automations can read the classification directly without downloading diagnostics. The attribute degrades gracefully when the value is absent.

  • engine/sun_geometry.pySunState enum, in_fov() method
  • diagnostics/builder.py — classification included in diagnostics output
  • sensor.pysun_state attribute on decision_trace sensor

🐛 Fixes

False manual override on cover returning from unavailable (#546, #551): The override detector in managers/manual_override/ treated any position delta as a manual override, even when the integration had never recorded a command target — for example, when a cover came back online after being unavailable. The fix threads has_recorded_target through the detection path: when no target has been recorded, the position-delta branch does not mark an override. A user-context change (explicit user action) still triggers the override regardless. The new constant _NON_POSITION_COVER_STATES centralizes the set of cover states excluded from position-delta detection.


🔧 Internal

The release script in scripts/release now verifies the built ZIP structure before publishing, guarding against a double-nested directory layout that causes HACS to install the integration at the wrong path (#544, #545). This is a distribution-only change with no runtime effect.


🧪 Testing

  • 4214 tests passing

Compatibility

  • Home Assistant 2026.3.0+

References

Adaptive Cover Pro ⛅ v2.26.1-nightly.202606081157

08 Jun 12:03

Choose a tag to compare

🌙 Nightly build from develop — unstable, for testing.
Auto-generated from the latest development code. Expect rough edges — not for production use.
Hit a problem? Open an issue and include this nightly version string.

What's Changed

  • chore: guard HACS zip install path in release tooling by @jrhubott in #545

Full Changelog: v2.26.0...v2.26.1-nightly.202606081157

Adaptive Cover Pro ⛅ v2.26.0

07 Jun 21:40

Choose a tag to compare

ℹ Using release notes from: release_notes/v2.26.0.md
2.26.0 is the stable release of the oscillating (drop-arm) awning cover type and the declarative config-field registry that made it possible. It also picks up six fixes found during beta: a timezone mismatch in sunset/sunrise boundary comparisons, a floor-clamp bug under manual override, motion detection gaps for media-player-only configs, a cover-type label derivation problem that caused oscillating saves to loop, a config summary omission, and an event re-anchoring issue. Schedule window start/end times are now published on the control_status sensor.


🎯 Highlights

  • New oscillating (drop-arm) awning cover type (#412): arm-sweep geometry, three new config fields, appears automatically in the cover-type picker.
  • Declarative config-field registry (config_fields.py + FieldSpec): every field's type, range, selector, and default defined once; OPTION_RANGES is derived from it rather than being defined separately in const.py.
  • CoverTypePolicy section API: build_section_schema(), section_order(), live_option_keys(), extra_field_keys(), disabled_config_keys(), plus __init_subclass__ auto-registration — cover types opt in with register=True and appear in SENSOR_TYPE_MENU without touching config_flow.py.
  • Schedule window exposed: schedule_start and schedule_end now published as attributes on the control_status sensor (#538).
  • Sunset/sunrise boundary comparisons fixed for non-UTC timezones, including a follow-up re-anchoring fix for next-event calculations (#531).

✨ Features

Oscillating (drop-arm) awning cover type (#412)

Drop-arm awnings extend downward at an angle rather than rolling straight out. The new engine in engine/covers/oscillating.py models this as an arm sweep: horizontal reach equals arm length times sin(sweep angle), and that reach maps linearly across the configured min/max angle range to an open percentage. AdaptiveOscillatingCover handles the forward and inverse transforms.

Three new config fields drive the geometry: CONF_ARM_LENGTH (physical arm length), CONF_AWNING_MIN_ANGLE and CONF_AWNING_MAX_ANGLE (the usable sweep range), and CONF_AWNING_HOUSING_OFFSET (distance from the wall mount to the window plane). The angle field is derived from position, so OscillatingAwningPolicy sets disabled_config_keys = {"angle"} to suppress it from the config UI automatically.

OscillatingAwningPolicy sets register=True, so CoverType.OSCILLATING_AWNING appears in the cover-type picker the moment the policy is imported. No manual entry in SENSOR_TYPE_MENU required.

  • engine/covers/oscillating.py — arm-sweep geometry and position mapping
  • cover_types/oscillating_awning.pyOscillatingAwningPolicy, OscillatingConfig
  • services/options_service.py — validators for the new fields
  • translations/en.json, translations/de.json, translations/fr.json — labels and descriptions for all four new fields

Declarative config-field registry and policy section API

Before this release, adding a cover type meant manually updating config_flow.py with new section builders, duplicating range definitions, and registering the type name in the picker by hand. Two changes close that loop.

config_fields.py introduces FieldSpec, a dataclass that captures type, range, selector, default, and validator for each config field. OPTION_RANGES is now built from the registry and re-exported to const.py rather than being defined separately there.

The CoverTypePolicy base class gains a section API: build_section_schema() generates the HA config-entry schema for a cover type's fields, section_order() declares the order those sections appear, live_option_keys() lists fields that should re-trigger a UI refresh, and extra_field_keys() and disabled_config_keys() let a policy include or suppress fields without subclassing for each variation. Policies set register=True in their class body to self-register in _POLICY_REGISTRY via __init_subclass__. The existing BlindPolicy, AwningPolicy, TiltPolicy, and VenetianPolicy are all migrated to this API.

config_dynamic.py holds the HA-aware section builders that config_flow.py previously defined inline. SENSOR_TYPE_MENU is now derived from _POLICY_REGISTRY, so the picker stays current as new policies register.

Schedule window attributes (#538, #539)

The control_status sensor now publishes schedule_start and schedule_end as attributes, exposing the automation window directly for automations and dashboards that need to know when the integration is active.


🐛 Fixes

Sunset/sunrise boundary compared in UTC (#531, #533): Entity-derived boundary times were compared in mismatched frames. compute_effective_default received a naive wall-clock time from the entity and compared it against a naive UTC time, producing an offset equal to the UTC offset — two hours in CEST, for example. _local_naive_to_utc_naive() in helpers.py converts the entity time before the comparison, so covers on non-UTC timezones activate sunset behavior at the correct time.

Next-event sunset/sunrise boundary re-anchored to today (#531, #541): A follow-up to the UTC fix. _read_time_entity() was computing the next-event boundary against a stale date rather than today, producing incorrect results after midnight. It now re-anchors to the current date before returning.

Floor position compared against actual position under manual override (#534, #536): The floor-clamp logic was comparing the floor against the tracked (last-commanded) position rather than the actual current position. Under manual override, where the actual and tracked positions diverge, this caused the floor check to pass when it should have blocked. The comparison now uses the actual cover position.

Motion detection enabled for media-player-only configs (#525): Instances configured with a media player but no binary motion sensors were skipping motion-detection setup entirely. Motion now initializes correctly in that configuration.

Cover-type label derived from policy, fixing oscillating save loop (#530, #532): _cover_type_label() in config_flow.py was not recognizing the oscillating awning type, causing the config save to cycle and reopen the form. The label is now derived from the policy registry, so all registered cover types resolve correctly.

Config summary surfaces dry-run, housing offset, and back-rotate lag (#528): The options summary shown at the end of config flow was missing three fields: the dry-run toggle, CONF_AWNING_HOUSING_OFFSET, and the venetian back-rotate lag. All three now appear.


🔧 Internal

OPTION_RANGES was previously defined as a plain dict in const.py, duplicating bounds already embedded in each field's selector definition. It now derives from the FieldSpec registry in config_fields.py and is re-exported to const.py for back-compat. Both the config-flow selectors and services/options_service.py validators now draw from the same source.


🧪 Testing

  • 3938 tests passing

Compatibility

  • Home Assistant 2026.3.0+

References

Adaptive Cover Pro ⛅ v2.26.1-nightly.202606060925

06 Jun 09:31

Choose a tag to compare

🌙 Nightly build from develop — unstable, for testing.
Auto-generated from the latest development code. Expect rough edges — not for production use.
Hit a problem? Open an issue and include this nightly version string.

What's Changed

  • feat: declarative per-cover-type config abstraction + oscillating awning (#412) by @jrhubott in #525
  • fix: surface dry-run, housing offset, back-rotate lag in config summary by @jrhubott in #528

Full Changelog: v2.25.2...v2.26.1-nightly.202606060925

Adaptive Cover Pro ⛅ v2.25.3

06 Jun 16:34

Choose a tag to compare

ℹ Using release notes from: release_notes/v2.25.3.md
One targeted fix for sunset/sunrise boundary comparisons in non-UTC timezones.


🐛 Fixes

Sunset/sunrise boundary now compared in UTC (#531, #533, #535)

In homes on a non-UTC timezone, entity-derived sunset/sunrise boundary times were compared in mismatched frames. compute_effective_default received a naive wall-clock time from the entity and compared it against a naive-UTC time, producing an offset equal to the UTC offset — two hours ahead in CEST, for example. Covers in those timezones would activate sunset behavior too early or not at all.

  • helpers.py gains _local_naive_to_utc_naive, which converts a naive local wall-clock time to naive UTC before the boundary comparison.
  • compute_effective_default now calls this helper, so both sides of the comparison are in the same frame.
  • Regression tests cover positive and negative UTC offsets.

Compatibility

  • Home Assistant 2026.3.0+

References

Adaptive Cover Pro ⛅ v2.26.0-beta.1

05 Jun 20:26

Choose a tag to compare

Pre-release

ℹ Using release notes from: release_notes/v2.26.0-beta.1.md
Beta 1 for 2.26.0. The headliner is the oscillating (drop-arm) awning, a new cover type that models the arm-sweep geometry of drop-arm awnings and maps horizontal reach to cover position. It ships alongside the config-system refactor that made adding it tractable: a declarative field registry (config_fields.py) and a policy section API that lets cover types register their own config sections without touching config_flow.py.

Please try the new cover type and report any geometry or positioning issues before this moves to stable.


🎯 Highlights

  • New oscillating (drop-arm) awning cover type (#412): arm-sweep geometry, three new config fields, appears automatically in the cover-type picker.
  • Declarative config-field registry (config_fields.py + FieldSpec): every field's type, range, selector, and default defined once; OPTION_RANGES is derived from it, removing the previous duplicate definitions in const.py.
  • CoverTypePolicy section API: build_section_schema(), section_order(), live_option_keys(), extra_field_keys(), disabled_config_keys(), plus __init_subclass__ auto-registration — cover types opt in with register=True and appear in SENSOR_TYPE_MENU without any config_flow.py changes.
  • Duplicate section-builder code removed from config_flow.py: section builders moved to config_dynamic.py, sourced from the policy registry.

✨ Features

Oscillating (drop-arm) awning cover type (#412)

Drop-arm awnings extend downward at an angle rather than rolling straight out. The new engine in engine/covers/oscillating.py models this as an arm sweep: horizontal reach equals arm length times sin(sweep angle), and that reach maps linearly across the configured min/max angle range to an open percentage. AdaptiveOscillatingCover handles the forward and inverse transforms.

Three new config fields drive the geometry: CONF_ARM_LENGTH (physical arm length), CONF_AWNING_MIN_ANGLE and CONF_AWNING_MAX_ANGLE (the usable sweep range), and CONF_AWNING_HOUSING_OFFSET (distance from the wall mount to the window plane). The angle field is derived from position, so OscillatingAwningPolicy sets disabled_config_keys = {"angle"} to suppress it from the config UI automatically.

OscillatingAwningPolicy sets register=True, so CoverType.OSCILLATING_AWNING appears in the cover-type picker the moment the policy is imported. No manual entry in SENSOR_TYPE_MENU required.

  • engine/covers/oscillating.py — arm-sweep geometry and position mapping
  • cover_types/oscillating_awning.pyOscillatingAwningPolicy, OscillatingConfig
  • services/options_service.py — validators for the new fields
  • translations/en.json, translations/de.json, translations/fr.json — labels and descriptions for all four new fields

Declarative config-field registry and policy section API

Before this release, adding a cover type meant manually updating config_flow.py with new section builders, duplicating range definitions, and registering the type name in the picker by hand. Two changes close that loop.

config_fields.py introduces FieldSpec, a dataclass that captures type, range, selector, default, and validator for each config field. OPTION_RANGES is now built from the registry and re-exported to const.py rather than being defined separately there.

The CoverTypePolicy base class gains a section API: build_section_schema() generates the HA config-entry schema for a cover type's fields, section_order() declares the order those sections appear, live_option_keys() lists fields that should re-trigger a UI refresh, and extra_field_keys() and disabled_config_keys() let a policy include or suppress fields without subclassing for each variation. Policies set register=True in their class body to self-register in _POLICY_REGISTRY via __init_subclass__. The existing BlindPolicy, AwningPolicy, TiltPolicy, and VenetianPolicy are all migrated to this API.

config_dynamic.py holds the HA-aware section builders that config_flow.py previously defined inline. SENSOR_TYPE_MENU is now derived from _POLICY_REGISTRY, so the picker stays current as new policies register.

  • config_fields.pyFieldSpec registry, derived OPTION_RANGES
  • cover_types/base.pyCoverTypePolicy section methods, __init_subclass__ auto-registration
  • config_dynamic.py — relocated section builders
  • config_flow.py — now sources builders from config_dynamic and the registry

🔧 Internal

OPTION_RANGES single-sourced from FieldSpec registry

OPTION_RANGES was previously defined as a plain dict in const.py, duplicating the same bounds already embedded in each field's selector definition. It now derives from the FieldSpec registry in config_fields.py and is re-exported to const.py for back-compat. Both the config-flow selectors and services/options_service.py validators now draw from the same source.


Compatibility

  • Home Assistant 2026.3.0+

References

Adaptive Cover Pro ⛅ v2.25.2

05 Jun 14:37

Choose a tag to compare

ℹ Using release notes from: release_notes/v2.25.2.md
Two command-reliability fixes and an internal refactor. The same-position gate now uses a tolerance band so covers with motor rounding don't receive redundant commands, and optimistic covers no longer drop their in-transit tracking prematurely.


🐛 Fixes

Position tolerance applied to same-position command gate (#507, #519)

The gate that suppresses redundant commands — used when the cover is already at the target — previously required exact position equality. Covers that report a position slightly off the commanded value due to motor or encoder rounding would fail the equality check and receive repeat commands unnecessarily.

  • managers/cover_command/__init__.py now evaluates a tolerance band instead of exact equality, so a near-match counts as already settled.
  • No change to behavior for covers that report positions precisely.

Wait-for-target preserved for optimistic covers mid-transit (#518, #520)

Optimistic covers report their target position immediately rather than tracking intermediate positions during travel. The integration was clearing wait-for-target tracking before the cover finished moving, causing it to treat the cover as settled too early and potentially issuing follow-up commands against a still-moving cover.

  • managers/cover_command/state_classifier.py now keeps wait-for-target active for optimistic covers while they remain in transit.

🔧 Internal

Pluggable subsystems + abstraction passes (#522)

Behavior-preserving internal refactor across the coordinator, managers, and pipeline.

  • Manual-override detection is now a pluggable subsystem: an OverrideDetector base with PositionDeltaDetector and TimeWindowDetector implementations, selectable via the new CONF_MANUAL_OVERRIDE_STRATEGY option (default via DEFAULT_MANUAL_OVERRIDE_STRATEGY). Stub testing support via StubDetector.
  • Climate-mode logic moved from branching coordinator code to declarative ClimateRule tables, evaluated through a ClimateContext and DetectionContext. OverrideDecision, DetectorConfig, SecondaryAxisCheck, and SecondaryAxisResult provide the supporting types.
  • Diagnostic event capture extracted into a shared EventRecorder.
  • Affected paths: const.py, coordinator.py, managers/common/*, managers/cover_command/*, managers/manual_override/*, managers/grace_period.py, managers/motion.py, managers/time_window.py, managers/weather.py, pipeline/handlers/* (including the new climate_modes.py).

Compatibility

  • Home Assistant 2026.3.0+

References

Adaptive Cover Pro ⛅ v2.25.2-nightly.202606041038

04 Jun 10:44

Choose a tag to compare

🌙 Nightly build from develop — unstable, for testing.
Auto-generated from the latest development code. Expect rough edges — not for production use.
Hit a problem? Open an issue and include this nightly version string.

What's Changed

  • fix: apply position tolerance to same-position command gate (#507) by @jrhubott in #519
  • fix: false manual override from optimistic covers mid-transit (#518) by @jrhubott in #520

Full Changelog: v2.25.1...v2.25.2-nightly.202606041038

Adaptive Cover Pro ⛅ v2.25.1

03 Jun 13:44

Choose a tag to compare

ℹ Using release notes from: release_notes/v2.25.1.md
One new venetian capability and a forecast accuracy fix. Custom position slots on venetian blinds can now adjust slat angle only, leaving the lift position under normal pipeline control. A separate fix corrects how the forecast evaluates sunrise/sunset gates — each sample in a full-day projection now uses its own timestamp rather than the current wall-clock time.


🚀 Features

Venetian tilt-only override for custom position slots (#514, #515)

Custom position slots on venetian blinds gain a per-slot CONF_CUSTOM_POSITION_TILT_ONLY option (default off, matching DEFAULT_CUSTOM_POSITION_TILT_ONLY = False).

  • When enabled, activating that custom slot adjusts only the slat tilt axis. The lift (primary) axis continues to follow normal pipeline logic — solar tracking, climate overrides, and other handlers all remain active for cover position.
  • When disabled (the default), the slot behaves exactly as before: full dual-axis override.
  • The new option is plumbed through pipeline/tilt_axis.py, pipeline/handlers/custom_position.py, and surfaces in the config flow and options service.

🐛 Fixes

Forecast sunset gate now evaluated at each sample's own timestamp (#516, #517)

The forecast engine's sunrise/sunset gate — which gates direct_sun_valid and next_sunrise checks — was previously evaluated once at wall-clock "now" and applied uniformly to every sample in a multi-hour projection. Samples far ahead in the day were therefore blocked or passed based on the current moment's sun state rather than their own position in time.

  • Each forecast sample is now evaluated at its own timestamp, so a full-day forecast correctly reflects sunrise and sunset transitions across the projection window.
  • Changes are in engine/covers/base.py, engine/sun_geometry.py, forecast.py, and sun.py.

Internal tooling: nightly tags are now excluded from the release-lifecycle previous-tag lookup, preventing CI from picking up interim nightly builds when computing changelogs.


Compatibility

  • Home Assistant 2026.3.0+

References