Skip to content

feat(covers): close the covers on the sun's elevation, and name those… - #25

Merged
Gamso merged 4 commits into
mainfrom
feat/cover_close_sun_elevation
Sep 22, 2026
Merged

Gamso merged 4 commits into
mainfrom
feat/cover_close_sun_elevation

Conversation

@Gamso

@Gamso Gamso commented Sep 20, 2026

Copy link
Copy Markdown
Owner

… left up

Two changes to the evening close, kept in one commit because they land in the same files and the same screens.

The closing time now comes from the sun's elevation. "Sunset + N minutes" is a delay you have to guess at; degrees above the horizon are a light level, the value sun.sun already publishes. Covers close when the descending sun reaches the configured elevation. Degrees say nothing about clock time, so the form turns the setting into tonight's time from the instance's own location — "At -2°, that is 21:41 tonight."

The minute offset is gone rather than kept alongside: two settings for one decision, with only one of them live, is a trap. The v3 -> v4 migration converts each entry's stored offset into the elevation it was landing on, averaged over the equinoxes and solstices and rounded to the half degree the form offers, so covers keep moving at the time they moved before. Measured with astral at 43.5N: sunset + 10 min is -2.5, sunset - 10 min is +1.5. The range offered (+-25) holds every value the old +-120 minutes could express, and an entry that never touched the offset is converted from the default it was silently running on.

Default is -2°: that is where the old default of sunset + 10 min landed, at every season, so a fresh install behaves like the old one.

The form refuses an elevation the sun never reaches at that location, checked against both solstices, instead of leaving the runtime to discover it at nightfall months later. What that rejects depends entirely on latitude: nothing at 43N, where the sun sweeps the whole range every day; +10 at 60N, where the midwinter sun stops at 6.6; everything above the polar circle in June. A rejected submission redisplays what was typed. The runtime keeps a fallback to plain sunset for what the form cannot check — a home that moved, an entry restored elsewhere — and the Cover Close Time sensor reports which of the two produced tonight's time.

binary_sensor.covers_left_open raises the skipped-cover warning where it can be acted on. A cover whose window sensor reads open is left up and, until now, only mentioned in the log, which nobody reads. The entity is a device_class problem, on as soon as one cover was skipped, listing the covers and the sensor that blocked each of them so a notification can name them. It clears when the next day's schedule is computed, not when the window closes: closing the window does not bring the cover down, so the warning has to outlive its cause. The list is persisted, so a reboot the same evening does not silence it.

Supporting changes: _covers_behind_an_open_window returns {cover: sensor} rather than a set, since the entity has to name the sensor and the callers only needed membership; _async_run_cover_checks pushes state to the entities, because the open/close timers fire between polls and the warning would otherwise show the previous value for up to five minutes; the astral lookups live at module level in cover_manager so the config flow and the migration can use them without a CoverManager, and go through get_astral_observer, since get_astral_location is deprecated for removal in HA 2027.7.

59 new tests (338 -> 397).

Gamso and others added 4 commits September 20, 2026 15:41
… left up

Two changes to the evening close, kept in one commit because they land in the
same files and the same screens.

**The closing time now comes from the sun's elevation.** "Sunset + N minutes"
is a delay you have to guess at; degrees above the horizon are a light level,
the value sun.sun already publishes. Covers close when the descending sun
reaches the configured elevation. Degrees say nothing about clock time, so
the form turns the setting into tonight's time from the instance's own
location — "At -2°, that is 21:41 tonight."

The minute offset is gone rather than kept alongside: two settings for one
decision, with only one of them live, is a trap. The v3 -> v4 migration
converts each entry's stored offset into the elevation it was landing on,
averaged over the equinoxes and solstices and rounded to the half degree the
form offers, so covers keep moving at the time they moved before. Measured
with astral at 43.5N: sunset + 10 min is -2.5, sunset - 10 min is +1.5. The
range offered (+-25) holds every value the old +-120 minutes could express,
and an entry that never touched the offset is converted from the default it
was silently running on.

Default is -2°: that is where the old default of sunset + 10 min landed, at
every season, so a fresh install behaves like the old one.

The form refuses an elevation the sun never reaches at that location, checked
against both solstices, instead of leaving the runtime to discover it at
nightfall months later. What that rejects depends entirely on latitude:
nothing at 43N, where the sun sweeps the whole range every day; +10 at 60N,
where the midwinter sun stops at 6.6; everything above the polar circle in
June. A rejected submission redisplays what was typed. The runtime keeps a
fallback to plain sunset for what the form cannot check — a home that moved,
an entry restored elsewhere — and the Cover Close Time sensor reports which
of the two produced tonight's time.

**binary_sensor.covers_left_open** raises the skipped-cover warning where it
can be acted on. A cover whose window sensor reads open is left up and, until
now, only mentioned in the log, which nobody reads. The entity is a
device_class problem, on as soon as one cover was skipped, listing the covers
and the sensor that blocked each of them so a notification can name them. It
clears when the next day's schedule is computed, not when the window closes:
closing the window does not bring the cover down, so the warning has to
outlive its cause. The list is persisted, so a reboot the same evening does
not silence it.

Supporting changes: _covers_behind_an_open_window returns {cover: sensor}
rather than a set, since the entity has to name the sensor and the callers
only needed membership; _async_run_cover_checks pushes state to the entities,
because the open/close timers fire between polls and the warning would
otherwise show the previous value for up to five minutes; the astral lookups
live at module level in cover_manager so the config flow and the migration
can use them without a CoverManager, and go through get_astral_observer,
since get_astral_location is deprecated for removal in HA 2027.7.

59 new tests (338 -> 397).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s moving

CI was testing Home Assistant 2026.2.3 while the local image ran 2026.9.2,
and get_astral_observer was added between the two — so the import failed
only on CI. The version was never chosen: the workflow pins Python 3.13,
which caps the resolvable pytest-homeassistant-custom-component, and that
caps Home Assistant.

This integration targets 2026.9 and above, so the workflows now run on
Python 3.14 (which resolves 2026.9.3), hacs.json refuses installation below
2026.9.0 instead of 2026.2.2, and the README says so.

The astral observer is built from hass.config.latitude / longitude /
elevation rather than imported. With the floor raised, get_astral_observer
would be available — but homeassistant.helpers.sun has already moved this
ground once, get_astral_location is deprecated for removal in 2027.7, and
building the observer is the single line that helper contains, against
config attributes that have been stable for years.

Verified on containers matching both jobs (python:3.14 + requirements_test.txt,
Home Assistant 2026.9.3): 397 passed, py_compile and validate.sh clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The closing elevation sat between "Earliest Open Time" and the per-mode
opening times, splitting the opening settings in two. It now comes first,
on its own, so the step reads as one closing field followed by everything
about opening. The translation files are reordered to match, which keeps
them diffable against the form.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Gamso
Gamso merged commit 2ea044e into main Sep 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant