Skip to content

Releases: jrhubott/adaptive-cover-pro

Adaptive Cover Pro ⛅ v2026.8.4-alpha.202608211315

Choose a tag to compare

🛠️ Develop 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 develop build version string.

What's Changed

New Contributors

Full Changelog: v2026.8.3...v2026.8.4-alpha.202608211315

Adaptive Cover Pro ⛅ v2026.8.4-alpha.202608201343

Choose a tag to compare

🛠️ Develop 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 develop build version string.

What's Changed

New Contributors

Full Changelog: v2026.8.3...v2026.8.4-alpha.202608201343

Adaptive Cover Pro ⛅ v2026.8.4-alpha.202608200537

Choose a tag to compare

🛠️ Develop 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 develop build version string.

What's Changed

New Contributors

Full Changelog: v2026.8.3...v2026.8.4-alpha.202608200537

Adaptive Cover Pro ⛅ v2026.8.4-alpha.202608200335

Choose a tag to compare

🛠️ Develop 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 develop build version string.

What's Changed

New Contributors

Full Changelog: v2026.8.3...v2026.8.4-alpha.202608200335

Adaptive Cover Pro ⛅ v2026.8.3

Choose a tag to compare

@jrhubott jrhubott released this 20 Aug 03:29

Highlights

2026.8.3 is an in-month patch: four bug fixes back-ported from develop, no new features, no configuration changes and nothing to migrate. Two of them fix things the UI told users outright wrongly. The occupancy countdown no longer reads "expired" for its entire life, which is what made the occupancy sensors look inverted (#1266). The Configuration Summary's cloud-suppression line said "weather in {sunny, …}" when suppression actually fires when the weather is not in that set — the exact opposite of what every install was configured to do, in all three shipped languages (#1301). The other two restore a blind spot's sky-compass wedge, which has published nothing at all on current stable (#1291), and let cleared optional config fields actually stay cleared (#1267).

Fixed

  • The occupancy timeout end time is measured from the timer, not the last detection (#1266): _motion_status_attrs derived motion_timeout_end_time from last_motion_time — the occupancy detection stamp — plus the timeout, but the no-motion timer starts on the clear edge. Anyone present for longer than the configured timeout pushed the published end time into the past the instant the timer actually started, so the card rendered "expires in expired" for the whole live countdown. With the default 300 second timeout, five minutes of continuous presence was enough.

    MotionManager now stamps _timeout_started_at when the timer starts and owns the formula behind a timeout_end_time property; the sensor publishes that verbatim and drops its private _timeout_seconds reach. The second half of the defect was the state after the countdown ends: _on_motion_timeout_expired set _motion_timeout_active without clearing the stamp, so the property kept returning a past timestamp for the entire no-occupancy state. All four exits from "timer counting" — record_motion_detected, cancel_motion_timeout, set_no_motion, and the timer firing naturally — now clear it, so the property honors its documented contract: a time while the timer runs, None otherwise. The attribute name and payload shape are unchanged, and the companion card already renders nothing when the attribute is absent, so no card release is needed.

    This is most of why the issue was filed as an inversion. The remaining half is wording: reason_i18n.py prints "occupancy timeout active" beside "No Occupancy" and "occupancy timeout not active" beside "Occupancy Detected". Both statements are true about the internal flag and both read as the inverse of the occupancy state next to them. That is a separate change across all three translation bundles and is not fixed here.

  • Cleared optional config fields round-trip (#1267): Emptying the Motion Template field in the options flow did not stick — the old template stayed in the config entry with no way to delete it from the UI. optional_entities() fills in None for any key voluptuous dropped from user_input, and it has to run before options.update() for a clear to overwrite anything. async_step_motion_override never called it.

    Auditing the rest of the options flow for the same defect class turned up two more paths with no clearing call at all. async_step_interp left CONF_INTERP_START / CONF_INTERP_END set, so a custom interpolation range could not be removed once configured. _render_blind_spot_slot's mapped comprehension skips any key missing from user_input, silently dropping a cleared right_gamma (slots 2 and 3) or elevation (all slots). All three now reuse the existing optional_entities() helper, inserted after each step's validation early-return so a rejected form clears nothing. left_gamma carries a schema default in every slot and is excluded on purpose.

  • Blind spots publish their gamma range again (#1291): The per-slot sub-key tuple in DiagnosticsBuilder._build_configuration copied only left, right, elevation and elevation_mode into the diagnostics configuration dict, so left_gamma / right_gamma never reached it. The sun sensor's gamma-preferred branch in _sun_position_attrs was therefore dead on that path, and because the options flow has written gamma keys only since v3.8, the legacy fallback found nothing either. Every configured blind spot published no blind_spot_range and no blind_spot_ranges at all — while the engine, which reads raw entry options, kept computing the correct position. That is why the card's sky compass showed no wedge while the blind sat exactly where it should.

    This is live on 2026.8.2: it carries both the four-key tuple and the gamma-only form keys, so any blind spot configured on current stable publishes nothing. Widening the tuple activates the already-correct gamma branch; no sensor change and no card change are needed. Entries migrated from before v3.8 hold both key sets, so gamma now wins over legacy in the published attribute, matching what the engine already does. That can shift a drawn wedge by up to the one-degree repair sliver clamp_gamma_pair applies — correct, but visible if you compare a screenshot from before and after.

    The gap that let this survive was the test shape: the new sensor tests drive a gamma-only entry through the real DiagnosticsBuilder instead of hand-feeding the configuration dict. Issue #1292, which layers a blind_spot_slots attribute on top of blind_spot_ranges, depends on this fix but is not shipped here.

  • The cloud-suppression summary states its trigger the right way round (#1301): The Configuration Summary rendered "skips sun tracking when weather in {sunny, partlycloudy, clear}". ClimateProvider sets is_sunny to weather_state in weather_condition and the priority-60 handler suppresses on not r.is_sunny, so suppression fires when the live weather state is not in the configured set. The summary told users the exact opposite of what their configuration does, and matched the option's own help text only by accident.

    cloud.weather_in now reads "weather not in {…}". Fixing the operator rather than restructuring the shared "skips sun tracking when …" stem keeps the terse, copula-free register of the sibling fragments it sits beside in the joined condition list (lux < X lx, cloud > Y%); those three are already correct, and rephrasing only the weather clause positively would either break that sentence or drag them in with it. Since #906 made the DE and FR summary bundles live, German and French users saw the same inverted wording, so all three bundles land together. The existing test pinned the buggy string as correct, which is how this shipped in the first place; its assertion now expresses the real semantics. A related finding from the same audit — the summary presenting the weather-state check as an OR triple — is tracked in #1302 and is not fixed here.

Testing

12,565 tests passing.

Compatibility

Home Assistant 2026.3.0+ and Python 3.11+. No migration runs for this release, and no new or changed configuration options. The companion Lovelace card (jrhubott/adaptive-cover-pro-card) is a separate repo with its own release cycle and needs no update for this patch.

Adaptive Cover Pro ⛅ v2026.8.3-alpha.202608190431

Choose a tag to compare

🛠️ Develop 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 develop build version string.

What's Changed

New Contributors

Full Changelog: v2026.8.2...v2026.8.3-alpha.202608190431

Adaptive Cover Pro ⛅ v2026.8.3-alpha.202608181437

Choose a tag to compare

🛠️ Develop 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 develop build version string.

What's Changed

New Contributors

Full Changelog: v2026.8.2...v2026.8.3-alpha.202608181437

Adaptive Cover Pro ⛅ v2026.8.3-alpha.202608180421

Choose a tag to compare

🛠️ Develop 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 develop build version string.

What's Changed

New Contributors

Full Changelog: v2026.8.2...v2026.8.3-alpha.202608180421

Adaptive Cover Pro ⛅ v2026.8.2

Choose a tag to compare

@jrhubott jrhubott released this 12 Aug 17:08

Highlights

2026.8.2 is an in-month patch: five bug fixes back-ported from develop, no new features, no configuration changes and nothing to migrate. Three of them cost users real cover movement. Manual overrides survive a Home Assistant restart again, after a regression that destroyed them on every reboot for every user (#1232). A blind with a blank start time and a configured end time no longer drives to its daytime default at local midnight (#1256). And the end-of-window reposition now actually fires instead of mistaking an ordinary in-window winner for an active override and skipping the move entirely (#1241). The remaining two fix a venetian drift-reset scope leak and a set_position_limits field that the Actions form never rendered.

Fixed

  • An unset manual-override toggle no longer wipes restored state (#1232): _update_manager_and_covers truthiness-tested manual_toggle, which is a tri-state bool | None. That conflated the None meaning "no switch entity has restored yet" with the False meaning "the user disabled manual-override detection". During platform setup the Integration Enabled switch fires a coordinator refresh three switch specs before Manual Override restores the flag, so the reset loop ran while the toggle was still None and destroyed every override the sensor had just rehydrated from the restore cache. Every user hit this on every reboot.

    Gating the reset on an explicit is False fixes it. This is a follow-on to #1019 and PR #1021, whose fix (populating manager.covers early) is correct and untouched.

  • A blank start time anchors to sunrise, not midnight (#1256): With no start time configured and an end time set, the operational window re-opened at local midnight. after_start_time mapped "no start configured" to True, putting the window's lower bound at midnight while _normalize_to_today pinned the end onto the new date. is_active flipped inactive to active at 00:00, check_transition fired on_window_open, and with every override handler declining in darkness the priority-0 default handler drove the cover to its daytime default.

    The night-hold branch added for #492 never reached these installs, because compute_effective_default short-circuits when sunset_pos is None and they had never configured a night position. TimeWindowManager now takes an optional sunrise_provider, and only the blank-start branch of after_start_time changed: when an end bound is configured it consults sunrise and fails open to True when none is available, so the window runs from sunrise to the configured end and never flips at midnight. The coordinator wires the provider as a closure over live SunData, converting astral's UTC-aware sunrise to naive local. The config summary renders "from sunrise" for that shape, with DE and FR synced. _resolve_start_datetime, _start_has_passed, window_explicitly_started and compute_effective_default are untouched. The mirror-image case, a start time set with the end left blank, is not fixed here; the shared _bound_is_configured helper prepares for it.

  • The pipeline refreshes before the end-time override guard (#1241): The end-of-window reposition is a one-shot fired from the reconciliation tick, and its override guard read whatever _pipeline_result the last coordinator cycle happened to write. This coordinator has no periodic update interval, so that result can predate end_time by up to a full reconciliation interval, and any ordinary in-window winner such as SOLAR, CLOUD or GLARE_ZONE then looked like an active override. The entire end-time move was skipped. managers/time_window.py latches the close edge before invoking the callback, so nothing retried, and outside the clock window no other dispatch path will move the cover.

    I refresh the pipeline immediately before consulting the guard. Re-evaluating with the clock window already closed makes every window-gated handler decline, so the winner is DEFAULT and the end-time position is sent. A genuinely active CUSTOM_POSITION, MANUAL or WEATHER slot still wins and is still deferred to, preserving the behaviour added in #895.

  • Sun-tracking minimum position is visible in set_position_limits (#1242): min_position_sun_tracking was already wired into FIELD_VALIDATORS and the _SECTION_POSITION_LIMITS frozenset the service is registered with, and it carried translation entries in all three languages, but services.yaml never declared the field. Home Assistant builds the Actions form strictly from services.yaml, so the option was invisible in the set_position_limits action even though calling the service with it worked. It now has a field block with a slider selector matching the backend range (0-99, step 1, %).

    The same service had the mirror-image gap: enable_position_matching was declared in services.yaml but had no translation entry in any language, so it rendered as raw English inside translated UIs. Both directions are now locked by a parity test modelled on the set_blind_spot guard from #247. The same class of drift in five other services is tracked separately in #1251 and is not fixed here.

  • Drift-reset ineligibility is inherited on the tilt retry (#1234): The sun_tracking_only drift-reset scope gate works correctly on the primary tilt send, but the #500 drift-retry re-send inside _verify_and_record_tilt called _send_tilt_command without threading drift_reset_eligible. The flag fell back to its True default and re-armed drift accumulation on a send the scope had already ruled ineligible, so a DEFAULT-controlled venetian tilt ran a full reset cycle. The scope gate itself was never wrong: it reads ControlMethod off PositionContext rather than the command's trigger label, which is why the original diagnosis landed one frame short of the real seam.

    _verify_and_record_tilt now takes drift_reset_eligible keyword-only and forwards it on the retry. Both verify call sites pass the raw parameter rather than the derived drift_reset_enabled, because the retry's own _send_tilt_command re-derives the _reset_in_progress and threshold folds itself. apply_user_tilt leaked the same flag and now passes _drift_reset_eligible(None), so a card or service tilt is eligible only under the all_tilt_commands scope. The retry still fires; only its accumulation is gated. This is a no-op under the default all_tilt_commands scope and for every non-venetian cover type.

Testing

12,584 tests passing.

Compatibility

Home Assistant 2026.3.0+ and Python 3.11+. No migration runs for this release, and no new or changed configuration options. The companion Lovelace card (jrhubott/adaptive-cover-pro-card) is a separate repo with its own release cycle.

Adaptive Cover Pro ⛅ v2026.8.1

Choose a tag to compare

@jrhubott jrhubott released this 08 Aug 00:34

Highlights

2026.8.1 is an in-month patch — five bug fixes back-ported from develop, no new features, no configuration changes and nothing to migrate. Two of them are behaviour fixes worth calling out: venetian tilt-only covers no longer hold their last daytime slat angle all night when a non-solar handler wins the pipeline (#1214), and the climate decision trace no longer stamps a season label on a decision that no season strategy produced (#1196) — which also unsticks the sunset force-send, the dual-panel heat blackout, and the day/night blackout fabric in that path. The other three are release-tooling and config-UI fixes: the Change Cover Type picker now offers and preselects the type you're already on, and the release script no longer leaks a log line into the published release notes.

Fixed

  • Default-position handlers get their own sunset tilt (#1214, #1218): #1153 correctly dropped tilt from the pipeline registry's _MERGEABLE set so a losing DefaultHandler could no longer leak default_tilt onto a winning handler. But three handlers answer with compute_default_position() and label themselves ControlMethod.DEFAULT without ever supplying a tilt of their own, so sunset_tilt and default_tilt became unreachable whenever one of them won — venetian tilt-only covers held their last daytime slat angle all night.

    I extracted the resolution out of DefaultHandler into pipeline.helpers.compute_default_tilt(), preserving sunset precedence, the default_tilt fallback, the #503 clamp on the non-sunset branch and the #128 unclamped sunset carve-out. ClimateHandler (LOW_LIGHT / TRACKING_SEASON_GATE), CloudSuppressionHandler, and MotionTimeoutHandler's return-to-default branch now each state their own tilt. MotionTimeoutHandler's hold branch stays untilted — a hold must hold. _MERGEABLE is untouched, so #1153's seam stays closed. I also refreshed the Default/Sunset Tilt copy across EN/DE/FR.

  • Climate labels follow the strategy, not the thermometer (#1196, #1209): ClimateHandler.evaluate() picked control_method from the is_summer/is_winter temperature predicates before consulting climate_strategy, so any non-season strategy reaching the label chain got stamped with a season label. A LOW_LIGHT fall-through to the default position reported SUMMER, and tilt GLARE_CONTROL reported SUMMER/WINTER even with the sun in the field of view.

    I gated the two season arms on a new _SEASON_STRATEGIES frozenset, so the season predicates are trusted only when a season strategy actually produced the position. LOW_LIGHT now reports DEFAULT and GLARE_CONTROL reports SOLAR in every season. This also corrects real downstream behaviour: on a LOW_LIGHT fall-through the sunset force-send is no longer suppressed, dual_panel no longer deploys the heat blackout, day_night_shade no longer forces the blackout fabric, and a tilt-only venetian carriage is no longer pinned.

  • Change Cover Type picker offers the current type, and DE/FR labels agree with the confirm screen (#1200, #1201, #1202, #1204): The picker filtered out the entry's own type and set no default, so Home Assistant highlighted whatever happened to be first. On a vertical blind that was the awning entry — whose English label also read "Horizontal blind" while the confirm screen called the same value "Horizontal Awning".

    I fixed the label, added the current type to the offered list with the suggested value pointing at it, and made picking it return to the menu instead of walking a no-op switch into the geometry step. Reverting a pending switch now gets its own confirm screen, so it no longer reads as "Vertical Blind to Vertical Blind". A new parity test locks every type the picker can offer to a label in all three languages.

  • scripts/release no longer leaks a log line into the published release body (#1187, #1207): get_release_notes() is captured via $(...), so its stdout is its return value. log_info wrote to stdout, which put an "ℹ Using release notes from: …" line at the top of the published GitHub release body. I gave log_info, log_success, log_warning, log_step and log_dry_run the same >&2 redirect log_error already had, so the whole helper family is safe inside a stdout-captured function rather than just the one call site that leaked. I also wrapped the main "$@" entry point in a BASH_SOURCE guard so the script can be sourced by a test without kicking off a real release. This release is the first one published with that fix active.

  • release-issue-lifecycle honours triage-exempt (#1188, #1205): The release lifecycle workflow tagged every referenced open issue released and then closed it in the sweep, with no way to exempt one — a commit only had to mention an issue number for that issue to be closed as fixed at the next stable cut. It bit twice during the v2026.8.0 release: #1173, whose only referencing commit rewrote xfail reasons and changed no behaviour, and #943, which carried reopened because the beta.1 work didn't cover the whole request.

    I added a triage-exempt guard to both mutation paths. In tagReferencedIssue() it goes first, before the awaiting-release/reopened stripping, so an exempt issue is left entirely untouched rather than partially mutated. The sweep needs its own gate because it queries GitHub by label.

📖 Documentation

  • Bug report form asks for less (#1200): I cut the required fields on the GitHub bug report form so filing an issue is less of a wall.

Testing

12,563 tests passing.

Compatibility

Home Assistant 2026.3.0+ and Python 3.11+. No migration runs for this release. The companion Lovelace card (jrhubott/adaptive-cover-pro-card) is a separate repo with its own release cycle.