Skip to content

Fix/audit 2026 10 - #26

Merged
Gamso merged 26 commits into
mainfrom
fix/audit-2026-10
Oct 14, 2026
Merged

Gamso merged 26 commits into
mainfrom
fix/audit-2026-10

Conversation

@Gamso

@Gamso Gamso commented Oct 3, 2026

Copy link
Copy Markdown
Owner

No description provided.

Gamso and others added 21 commits October 3, 2026 19:12
180 test bodies drove coroutines through
asyncio.get_event_loop().run_until_complete() from synchronous tests.
get_event_loop() without a running loop is deprecated and raises on
recent Python versions unless a plugin happens to have installed a
loop first, which made the suite depend on collection order.

The tests are now async functions run by pytest-asyncio (asyncio_mode
= auto) and await the coroutines directly; the helpers they share
became coroutines as well. No assertion changed.

Audit: T1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Schedulers were stored per day mode display label and the form was built
from the flow's raw data instead of the localized defaults. On a French
instance that never opened the Mapping step, the form offered the English
labels (Home, Work, ...) while the coordinator ran on Maison/Travail, so
schedulers_per_mode.get(day_mode) never matched: no scheduler was ever
turned on and all of them were turned off on every mode change. Renaming
a label broke the assignment the same way.

- The schedulers form is built from the effective data and its fields
  are named after the stable mode key (schedulers_home, ...), translated
  in en/fr.
- The coordinator resolves the active schedulers through day_mode_key.
- Config entry v5 migration re-keys the stored map: current label, key
  in another case, or a default label of any supported language (the
  broken French case) all resolve to the key; unknown labels are kept and
  logged, labels resolving to the same key are merged.
- Tests that froze the label-keyed format now expect keys; new tests
  cover the French instance that skipped Mapping, a rename, and the
  migration paths.

Audit: B1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An unavailable or unknown work calendar was read as "no event": a CalDAV
or Google outage on a remote-work Tuesday switched the day to Work, ran
the schedulers, then switched back once the calendar returned; an
absence set by a calendar event was cancelled the same way. A missing
calendar at boot also returned early, skipping the whole cover schedule.

- A work or holiday calendar that is missing, unavailable or unknown no
  longer drives the mode: it is kept, the outage is warned about once
  and its end logged.
- The cover schedule, its timers and the cover checks run whatever the
  calendar state. A schedule computed while the calendar was unreadable
  is provisional and computed again once the calendar answers, so a
  mode change at that point still applies to today's covers.
- manifest: after_dependencies on calendar, so the calendar integration
  is set up first when both load at startup.

Audit: B2, P4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'skip' cleared cover_open_time, and heat protection (proactive and
reactive) as well as its window required it: in Away mode with 'skip'
during a heatwave the south cover was never closed, on exactly the day
nobody was home to do it.

- 'skip' now only suppresses the opening. Heat protection's window
  starts at the opening time when there is one, otherwise at the
  sunrise-based time (sunrise floored at the earliest opening time, or
  that earliest time when sunrise is unknown). The precise timer fires
  at that start on skip days.
- Heat protection sends nothing when every protected cover already
  reads closed: on a skip day they are typically still down from the
  night, and a My press or a stop would only move them up.

Audit: B3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The store only kept the mode keys and the absence flag. A manual change
with a 120 min override followed by an HA restart lost _override_until,
and the first calendar sync after the reboot overwrote the mode picked
by hand.

The override end is now saved with the rest of the state and restored
when it is still in the future.

Audit: B4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cover, button and switch services were called from the coordinator
update without any error handling. A HomeAssistantError (a Somfy API
timeout, a removed entity) failed the update, and every HomeShift
select, sensor and binary sensor went unavailable until the next poll.
The proactive close also marked the day as checked before closing, so a
failed close was never retried that day.

- Every cover/button call goes through one helper that logs a failure
  as a warning and reports it. The daily open, the daily close and both
  heat protection paths only record an action as done once it
  succeeded; a failure is retried on the next poll (close_cover is
  idempotent, a My press on a cover already at its position is a no-op).
- Scheduler switches are now called with blocking=True inside the same
  kind of guard: a failure becomes visible in the log instead of being
  lost in a fire-and-forget task, and cannot fail the update.

Audit: P1, P3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
async_config_entry_first_refresh() arms the next-mode and cover timers,
but their cancellation was only registered with async_on_unload after
the platforms were forwarded. When the first refresh failed after
arming them, HA raised ConfigEntryNotReady and retried with a new
coordinator while the old one's timers stayed armed, later running
async_sync_calendar on an orphan coordinator (real switch and cover
calls).

The cancellations are now registered right after the coordinator is
built; HA runs the callbacks registered so far when setup fails.

Audit: P2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every entity hard-coded an English _attr_name, so the `entity` section
of the translations was never used (and select.day_mode was even named
"HomeShift" there). The device info dict was duplicated in each
platform.

- Entities use _attr_has_entity_name with a translation_key: names now
  follow the instance language. en.json gets the missing Cover Open
  Time name and the right Day Mode name.
- Home Assistant derives a new entity's object id from the translated
  name for the languages it supports natively, French included, which
  would have registered select.homeshift_mode_jour on a French install.
  The entity id is therefore pinned to the existing English one
  (select.homeshift_day_mode, number.homeshift_override_duration,
  sensor.homeshift_next_mode, ...): new installs get the ids the README,
  automations and the HomeShift card expect, whatever the language.
  Existing registry entries are untouched.
- One shared DeviceInfo, declared as a service device (no hardware).
- First tests on a real Home Assistant instance (fixture hass): entity
  ids in en and fr, translated names, the service device.

Audit: A1, A4 (device info)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cover open/close time sensors, the covers-left-open sensor and the
heat protection sensor are only created while their feature is
configured. Removing the feature stopped creating them, but their
entity registry entries stayed behind as restored, unavailable
entities.

Each platform now removes the registry entries of the conditional
entities it no longer creates. Covered by a reload-on-options test on a
real Home Assistant instance.

Audit: A5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- async_remove_entry deletes the two .storage files the coordinator and
  the cover manager keep per entry; they were left orphaned when the
  integration was deleted.
- diagnostics.py exposes the entry configuration, the coordinator data
  and today's cover schedule for bug reports (no secret in there: entity
  ids, mode maps and times only).
- manifest integration_type is now "service": HomeShift drives other
  entities and has no hardware, which "hub" implied.

Audit: A4

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The translations named the Away display field day_display_absence
  while the form generates day_display_away (field name built from the
  mode key), so the raw field name was shown. Renamed in en/fr, config
  and options.
- The cover action and the per-mode opening time option labels were
  picked in Python from hass.config.language (French or English only,
  the opening labels English only). They now come from a "selector"
  section of the translations through the selectors' translation_key,
  like any other label.
- Tests check that every generated display field and selector option has
  a translation in both languages.

Audit: A2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- manifest: declare the astral requirement cover_manager imports
  directly (it only worked as a transitive dependency of HA core); a
  lower bound, so HA's own pin always satisfies it. Point documentation
  to Gamso/homeshift (was Gamso/day_mode), add issue_tracker, and sort
  the keys the way hassfest expects.
- config_flow: the config flow and the options flow carried two copies
  of the same eight steps (~170 lines). They now share one mixin; each
  flow keeps only its entry point and its final step.
- Remove the unused THERMOSTAT_OFF_KEY constant and its import.
- Remove custom_components/homeassistant, a symlink to a path of the
  author's dev container.
- hacs.json: drop the "domain" key, unknown to HACS and spelled
  home_shift.
- .vscode: fix the leftover day_mode workspace path.

Audit: A3, A6

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The suite drove the coordinator against a MagicMock hass, so nothing
covered a real setup/unload, the reload on options, the timers of a
failed first refresh, or the French setup path behind B1.

- New tests on the hass fixture of pytest-homeassistant-custom-component:
  setup then unload (services removed, entities unavailable, timers
  cancelled), options saved -> entry reloaded with a new coordinator, a
  failing first refresh (SETUP_RETRY, no timer left armed), an
  unavailable calendar keeping the Remote mode then resuming, and the
  French config flow that skips Mapping and must still turn on the
  work-day scheduler.
- The day_mode setter existed in the coordinator "for test setup only".
  It is gone; tests use a set_day_mode() helper from conftest instead.

Audit: T1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The HomeShift card needs to open or close the managed covers when the
opening or closing time is clicked. Nothing exposed that: the daily
open and close only ran from the schedule.

- New button platform, created under the same condition as the cover
  open/close time sensors (at least one daily cover): Open covers and
  Close covers, entity ids button.homeshift_open_covers and
  button.homeshift_close_covers (pinned like the other entities),
  unique ids <entry_id>_open_covers / <entry_id>_close_covers,
  translation keys open_covers / close_covers (en/fr), icons
  mdi:window-shutter-open / mdi:window-shutter. Plain buttons, not
  coordinator entities, so they stay available; removed from the
  registry once the daily covers are unconfigured.
- New services homeshift.open_covers and homeshift.close_covers, without
  fields (services.yaml, en/fr translations), removed with the last
  entry like the other services.
- Both reuse the cover manager's code: open_cover on every daily cover
  whatever the time or 'skip'; the close follows the evening close rules
  (cover behind an open window left up and warned about, My position
  button pressed instead of close_cover), now shared in one helper.
  They run under the same action lock, and a failed service call is
  logged, never raised.
- Decision: a manual action does not mark the day's scheduled open or
  close as done and does not touch the covers-left-open warning. The
  scheduled actions still run at their time; an open command on open
  covers is a no-op, and a manual close in the morning should not
  silently cancel that day's opening. Documented in the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The CI only compiled the Python files and parsed the JSON ones, on
actions/checkout@v3 and setup-python@v4. requirements_test.txt was not
pinned and pytest had three configurations (pyproject.toml, setup.cfg,
.devcontainer/pytest.ini).

- validate.yml: hassfest (home-assistant/actions), the HACS action and
  a ruff job next to the existing validate.sh, on checkout@v5 and
  setup-python@v6. The syntax/JSON/file checks were already done by
  validate.sh and are no longer duplicated.
- ruff configured in pyproject.toml (E, F, W, B, UP, line length 180,
  the repo's black setting), pinned in requirements_dev.txt; the
  findings are fixed (unused imports and variables, Callable from
  collections.abc, datetime.UTC, trailing whitespace in a script).
- requirements_test.txt pins pytest-homeassistant-custom-component
  0.13.363, i.e. Home Assistant 2026.9.0, the minimum hacs.json
  declares, plus icalendar. The suite also passes on 2026.10.0b0.
- pyproject.toml is the only pytest configuration (testpaths added);
  setup.cfg and .devcontainer/pytest.ini are removed.
- The 2x brand icon was named icon@2x.png.png; renamed icon@2x.png.

Audit: T2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing here ships with the integration; these are the dev scripts and
container the audit flagged.

- scripts/init_calendars.py took the Home Assistant token as a command
  line argument, visible to every local user (ps, /proc/<pid>/cmdline)
  and kept in the shell history. It now reads HA_TOKEN from the
  environment, or prompts for it without echo.
- devcontainer: ~/.ssh is mounted read-only. The container can still use
  the keys, but no longer modify them, authorized_keys or the SSH
  config.
- scripts/starts_ha.sh downloaded the scheduler component and card
  without any integrity check, then ran them inside Home Assistant and
  the browser. Each download is now checked against a pinned SHA-256
  and deleted on mismatch. The card's digest is the one GitHub
  publishes for the release asset; GitHub publishes none for the older
  scheduler.zip, so its pin is left empty: the script prints the
  computed digest with a warning until the author checks and pins it.

Audit: S

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Entity ids: the README named select.day_mode,
  binary_sensor.covers_left_open, ... while the real ids carry the
  device prefix (select.homeshift_day_mode, ...). Fixed everywhere,
  including the notification automation example, which would never
  have triggered; states that the ids do not depend on the language.
- The scan interval is a fixed 5 minutes, not a parameter; the holiday
  calendar is required, not optional; the day modes are four fixed keys
  that can be renamed, not a free list; the next-mode prediction looks
  7 days ahead, not 2.
- Documents the behaviour this branch changed: a calendar that cannot
  be read keeps the mode, covers keep running without it, schedulers
  are keyed by mode key (with the 1.9.x upgrade note), heat protection
  runs on 'skip' days from the sunrise-based time and leaves closed
  covers alone, failed commands are logged and retried, the manual
  override survives a restart, stores are removed with the entry,
  diagnostics, and the missing binary_sensor.homeshift_cover_heat_active
  entry.
- .github/copilot-instructions.md: same corrections for contributors
  (file map, poll interval, test harness).

Audit: D1

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hassfest rejects it: "Requirement astral>=2.2 is a dependency of Home
Assistant itself and must not be listed in the manifest of a custom
integration". Keep using it undeclared and say why at the import.

Audit: A3 (revised after CI)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some covers need to stay put for a while — a bedroom kept dark for a few
days — without disabling the whole cover schedule.

- homeshift.inhibit_covers takes covers out of the automation for a
  duration, until a date, or until resumed; homeshift.resume_covers hands
  them back (all of them when no cover is given).
- An inhibited cover receives neither the daily open nor the evening
  close, its window is not checked, heat protection leaves it alone, and
  the open/close-now buttons and services skip it. Inhibiting or resuming
  never moves a cover; a missed open/close is not caught up, while heat
  protection stays armed for the rest of the day.
- Inhibitions are persisted with the cover state, dropped at the first
  poll after they end, and listed in the diagnostics.
- sensor.homeshift_covers_inhibited reports how many covers are
  inhibited, each one with its end, and every cover that can be
  inhibited, for dashboards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVhrB9KRnmbFZRPHNHUev
test_names_follow_the_instance_language asserted that the day mode reads
"Maison", which only holds on a weekend or a holiday: the test failed on
any work day. It checks that names follow the instance language, so assert
that the state is one of the French display names instead.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVhrB9KRnmbFZRPHNHUev
On Saturday 2026-10-10 the covers opened at 07:44, the Remote time, instead
of the 08:30 weekend time. At 00:00:00 the calendar entity still reported
Friday's all-day "Télétravail" event — its state updates on its own
schedule, a few seconds after the event ends — so the day mode stayed
Remote and the day's schedule was computed with it. The switch to Home at
the next poll did not move the opening time: the schedule was only
computed once per day.

- While the morning open has not run, a day-mode change (automatic or
  picked by hand) recomputes today's open time and reschedules the timers.
  This also covers the other direction: an event starting at midnight
  that the calendar entity only shows a poll later. Once the covers
  opened, a later change leaves the day's times alone.
- A calendar event whose end_time is already past is ignored, so the
  midnight switch happens on time instead of five minutes later.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVhrB9KRnmbFZRPHNHUev
@Gamso
Gamso force-pushed the fix/audit-2026-10 branch from 1fbe129 to f3dde9d Compare October 10, 2026 06:06
Gamso and others added 5 commits October 10, 2026 08:20
…close

The evening close sent one close_cover to every cover at once, including
the ones already lowered by hand. A cover that rejected that command made
the whole call fail, and covers with a My position button were even
raised back up by the press.

Covers already reading closed now receive nothing, and each remaining
cover gets its own call, so one failure no longer affects the others
and the retry only resends to the covers still open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The main menu now lists Calendars, Day modes, Schedulers and Covers.
  Each entry shows a one-line summary of its current settings.
- Covers is a sub-menu holding Managed covers, Opening and closing
  times, and Heat protection.
- The menu lists the sections changed and not saved yet, and warns that
  closing with ✕ discards them.
- Each form's button says where it returns: "Save and back to ...".
- Day modes: the rules come first, the display names are folded, and the
  mode dropdowns show display names instead of raw keys.
- Schedulers: one folded section per mode, titled with the mode's name
  and scheduler count. Chips show the friendly name only.
- Heat protection and opening/closing times are split into sections:
  trigger/action, evening close/morning opening.
- Managed covers are listed by name, and gain an "Edit a cover" entry.

The stored options keep their flat format, so no migration is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The evening close leaves a cover up when its window sensor reports the
window open. A new option, "Close as soon as the window is shut" (off by
default), lets that cover close the moment the window is shut, or go to
its My position if it has a button.

- The wait lasts until the next morning open and is only acted on at
  night, so past midnight still counts. A window shut in the daytime,
  even on a skip day, never lowers a cover.
- The window sensors are listened to while the option is on. The poll
  catches up a window shut while Home Assistant was down, since the wait
  is persisted.
- A cover lowered by hand or no longer managed stops waiting. An
  inhibited cover keeps waiting, and so does one whose window sensor is
  unavailable.
- A cover closed this way drops off covers_left_open and the entities
  refresh at once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… across upgrades

- A thermostat mode change now switches the climates of tagged schedulers
  to the matching HVAC mode (or off) and runs the current slot of the
  enabled schedulers of that mode.
- Covers left up by an open window at the evening close keep waiting for
  it to shut after an upgrade from a version that did not store the wait.
- Diagnostics expose close_when_window_shuts and waiting_for_window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Gamso
Gamso merged commit c20c840 into main Oct 14, 2026
5 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