Releases: jrhubott/adaptive-cover-pro
Adaptive Cover Pro ⛅ v2.27.1-nightly.202606091034
🌙 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
Full Changelog: v2.27.0...v2.27.1-nightly.202606091034
Adaptive Cover Pro ⛅ v2.27.0
ℹ 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_MOVEMENTSoption 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
SunStateenum (HITTING,IN_FOV_NOT_VALID,OUTSIDE_FOV) is computed per cycle, surfaced in diagnostics, and exposed assun_stateon thedecision_tracesensor 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 mathpipeline/helpers.py—solar_position_from_geometry,default_position_with_limits,apply_config_limitsextracted 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.py—quantize_to_coverage_steps()pipeline/handlers/solar.py— applies quantization whenCONF_MINIMIZE_MOVEMENTSis enabledservices.yaml—CONF_MINIMIZE_MOVEMENTS,CONF_MAX_COVERAGE_STEPSexposedtranslations/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.py—SunStateenum,in_fov()methoddiagnostics/builder.py— classification included in diagnostics outputsensor.py—sun_stateattribute ondecision_tracesensor
🐛 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
🌙 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
Full Changelog: v2.26.0...v2.26.1-nightly.202606081157
Adaptive Cover Pro ⛅ v2.26.0
ℹ 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_RANGESis derived from it rather than being defined separately inconst.py. CoverTypePolicysection API:build_section_schema(),section_order(),live_option_keys(),extra_field_keys(),disabled_config_keys(), plus__init_subclass__auto-registration — cover types opt in withregister=Trueand appear inSENSOR_TYPE_MENUwithout touchingconfig_flow.py.- Schedule window exposed:
schedule_startandschedule_endnow published as attributes on thecontrol_statussensor (#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 mappingcover_types/oscillating_awning.py—OscillatingAwningPolicy,OscillatingConfigservices/options_service.py— validators for the new fieldstranslations/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
🌙 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
ℹ 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.pygains_local_naive_to_utc_naive, which converts a naive local wall-clock time to naive UTC before the boundary comparison.compute_effective_defaultnow 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
ℹ 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_RANGESis derived from it, removing the previous duplicate definitions inconst.py. CoverTypePolicysection API:build_section_schema(),section_order(),live_option_keys(),extra_field_keys(),disabled_config_keys(), plus__init_subclass__auto-registration — cover types opt in withregister=Trueand appear inSENSOR_TYPE_MENUwithout anyconfig_flow.pychanges.- Duplicate section-builder code removed from
config_flow.py: section builders moved toconfig_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 mappingcover_types/oscillating_awning.py—OscillatingAwningPolicy,OscillatingConfigservices/options_service.py— validators for the new fieldstranslations/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.py—FieldSpecregistry, derivedOPTION_RANGEScover_types/base.py—CoverTypePolicysection methods,__init_subclass__auto-registrationconfig_dynamic.py— relocated section buildersconfig_flow.py— now sources builders fromconfig_dynamicand 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
ℹ 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__.pynow 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.pynow 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
OverrideDetectorbase withPositionDeltaDetectorandTimeWindowDetectorimplementations, selectable via the newCONF_MANUAL_OVERRIDE_STRATEGYoption (default viaDEFAULT_MANUAL_OVERRIDE_STRATEGY). Stub testing support viaStubDetector. - Climate-mode logic moved from branching coordinator code to declarative
ClimateRuletables, evaluated through aClimateContextandDetectionContext.OverrideDecision,DetectorConfig,SecondaryAxisCheck, andSecondaryAxisResultprovide 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 newclimate_modes.py).
Compatibility
- Home Assistant 2026.3.0+
References
Adaptive Cover Pro ⛅ v2.25.2-nightly.202606041038
🌙 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
ℹ 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, andsun.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+