|
| 1 | +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. |
| 2 | + |
| 3 | +--- |
| 4 | + |
| 5 | +## 🎯 Highlights |
| 6 | + |
| 7 | +- **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. |
| 8 | +- **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. |
| 9 | +- **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. |
| 10 | +- **False manual override suppressed** ([#546], [#551]): covers returning from unavailable no longer trigger a spurious manual override when no prior command target exists. |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## ✨ Features |
| 15 | + |
| 16 | +### Forecast/runtime parity ([#556]) |
| 17 | + |
| 18 | +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. |
| 19 | + |
| 20 | +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. |
| 21 | + |
| 22 | +`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. |
| 23 | + |
| 24 | +- `forecast.py` — now calls shared primitives instead of duplicating position math |
| 25 | +- `pipeline/helpers.py` — `solar_position_from_geometry`, `default_position_with_limits`, `apply_config_limits` extracted here |
| 26 | + |
| 27 | +### Opt-in movement minimization ([#555]) |
| 28 | + |
| 29 | +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. |
| 30 | + |
| 31 | +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. |
| 32 | + |
| 33 | +- `position_utils.py` — `quantize_to_coverage_steps()` |
| 34 | +- `pipeline/handlers/solar.py` — applies quantization when `CONF_MINIMIZE_MOVEMENTS` is enabled |
| 35 | +- `services.yaml` — `CONF_MINIMIZE_MOVEMENTS`, `CONF_MAX_COVERAGE_STEPS` exposed |
| 36 | +- `translations/en.json`, `translations/de.json`, `translations/fr.json` — labels and descriptions |
| 37 | + |
| 38 | +### Sun-state classification in diagnostics and decision trace ([#552], [#553], [#554]) |
| 39 | + |
| 40 | +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. |
| 41 | + |
| 42 | +`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. |
| 43 | + |
| 44 | +- `engine/sun_geometry.py` — `SunState` enum, `in_fov()` method |
| 45 | +- `diagnostics/builder.py` — classification included in diagnostics output |
| 46 | +- `sensor.py` — `sun_state` attribute on `decision_trace` sensor |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## 🐛 Fixes |
| 51 | + |
| 52 | +**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. |
| 53 | + |
| 54 | +--- |
| 55 | + |
| 56 | +## 🔧 Internal |
| 57 | + |
| 58 | +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. |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +## 🧪 Testing |
| 63 | + |
| 64 | +- 4214 tests passing |
| 65 | + |
| 66 | +--- |
| 67 | + |
| 68 | +## Compatibility |
| 69 | + |
| 70 | +- Home Assistant 2026.3.0+ |
| 71 | + |
| 72 | +--- |
| 73 | + |
| 74 | +## References |
| 75 | + |
| 76 | +[#544]: https://github.com/jrhubott/adaptive-cover-pro/issues/544 |
| 77 | +[#545]: https://github.com/jrhubott/adaptive-cover-pro/issues/545 |
| 78 | +[#546]: https://github.com/jrhubott/adaptive-cover-pro/issues/546 |
| 79 | +[#551]: https://github.com/jrhubott/adaptive-cover-pro/issues/551 |
| 80 | +[#552]: https://github.com/jrhubott/adaptive-cover-pro/issues/552 |
| 81 | +[#553]: https://github.com/jrhubott/adaptive-cover-pro/issues/553 |
| 82 | +[#554]: https://github.com/jrhubott/adaptive-cover-pro/issues/554 |
| 83 | +[#555]: https://github.com/jrhubott/adaptive-cover-pro/issues/555 |
| 84 | +[#556]: https://github.com/jrhubott/adaptive-cover-pro/issues/556 |
0 commit comments