100 Commits
Author SHA1 Message Date
Andrew Grimberg e9dc8c26ed Merge pull request #921 from tykeal/feat/require-ha-2026-11
Feat!: Require Home Assistant 2026.11
2026-11-05 15:12:00 -08:00
Andrew GrimbergandCopilot 76b68e1fa5 Docs(release): Add note for HA 2026.11 minimum
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-05 15:07:08 -08:00
Andrew GrimbergandCopilot e6bbc93623 Docs: Drop pre-2026.11 installation guidance
Home Assistant 2026.11 is now the minimum, so the README no longer
needs the HACS install path and restart note for older releases.
Point the install button at the documented marketplace_repository My
Home Assistant redirect instead of the legacy hacs_repository one,
and drop the matching sentence from the unreleased Marketplace note.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-05 15:07:05 -08:00
Andrew GrimbergandCopilot edef45e495 Test: Run entity_id rename tests unconditionally
The in-place rename test was skipped, and the timer assertions were
guarded, on Home Assistant releases without async_entity_id_changed.
2026.11 is now the minimum, so drop the capability check and always
run them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-05 15:06:25 -08:00
Andrew GrimbergandCopilot 249385ea23 Refactor: Drop pre-2026.11 entity_id hook guard
async_chain_entity_id_changed only existed to skip the parent
async_entity_id_changed call on Home Assistant releases that lack it.
With 2026.11 as the minimum the hook always exists, so call it through
super() directly and remove the helper module. Each class still
defines the hook itself, which is what opts it in to in-place
entity_id changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-05 15:06:18 -08:00
Andrew GrimbergandCopilot 4e9c7ea127 Feat!: Require Home Assistant 2026.11
The test suite now runs only against Home Assistant 2026.11 (the
pytest-homeassistant plugin pins it), and Rental Control relies on
2026.11 behaviour such as in-place entity_id changes. Raise the
declared minimum to match: hacs.json, which HACS and the Marketplace
use to choose which release to offer, and the remaining dev floors
in pyproject.toml.

Users on older Home Assistant releases keep being offered v3.9.x.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-05 15:06:13 -08:00
Andrew Grimberg dc4a276e3d Merge pull request #920 from tykeal/test/pytest-homeassistant
Test: Switch to the pytest-homeassistant plugin
2026-11-05 15:02:46 -08:00
Andrew GrimbergandCopilot da3f163a90 Build: Declare pytest-cov as a test dependency
pytest-homeassistant-custom-component pulled pytest-cov in
transitively; pytest-homeassistant does not, so a clean install of the
test extra could no longer run the README's coverage command. Declare
it explicitly and list it with the other declared dependencies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-05 14:57:37 -08:00
Andrew GrimbergandCopilot 60eed15652 Test: Switch to the pytest-homeassistant plugin
pytest-homeassistant-custom-component is deprecated upstream and stuck
at a release pinning Home Assistant 2026.10.4. Home Assistant core now
publishes the centrally managed pytest-homeassistant package, versioned
with (and pinning) core itself.

Replace the test extra with pytest-homeassistant>=2026.11.0, rename the
pytest_homeassistant_custom_component imports, and drop the explicit
pytest_plugins registration since the new plugin loads through its
entry point. Add "-p no:respx" to addopts as its README requires, so
Home Assistant can install its httpx alias before respx is imported.

The new plugin pins homeassistant==2026.11.0, so the test extra's core
floor moves to 2026.11.0 to match and the lock now resolves 2026.11.0.
The runtime minimum (hacs.json, dev floors) is raised separately.

The autouse enable_custom_integrations wrapper is kept: custom
integrations are now enabled by default, but the wrapper also gives
every test a running hass instance that some unit tests rely on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-05 14:53:00 -08:00
Andrew Grimberg 967a2ee851 Merge pull request #919 from tykeal/dependabot/pre_commit/pre-commit-hooks-6a68134d46
Chore: Bump the pre-commit-hooks group with 3 updates
2026-11-05 14:48:57 -08:00
Andrew Grimberg e525cfe835 Merge pull request #918 from tykeal/dependabot/github_actions/lfreleng-actions/security-workflows/dot-github/workflows/openssf-scorecard.yaml-0.9.7
Chore: Bump lfreleng-actions/security-workflows/.github/workflows/openssf-scorecard.yaml from 0.9.5 to 0.9.7
2026-11-05 14:48:54 -08:00
Andrew Grimberg 8a74f9e7c8 Merge pull request #917 from tykeal/dependabot/github_actions/lfreleng-actions/security-workflows/dot-github/workflows/codeql.yaml-0.9.7
Chore: Bump lfreleng-actions/security-workflows/.github/workflows/codeql.yaml from 0.9.5 to 0.9.7
2026-11-05 14:48:51 -08:00
Andrew Grimberg 4f8ce65161 Merge pull request #916 from tykeal/dependabot/github_actions/lfreleng-actions/tag-validate-action-1.1.8
Chore: Bump lfreleng-actions/tag-validate-action from 1.1.7 to 1.1.8
2026-11-05 14:48:47 -08:00
Andrew Grimberg 05602ef62d Merge pull request #913 from tykeal/dependabot/github_actions/lfreleng-actions/grype-scan-action-0.5.0
Chore: Bump lfreleng-actions/grype-scan-action from 0.4.0 to 0.5.0
2026-11-05 14:48:44 -08:00
Andrew Grimberg 0c1b92a9cf Merge pull request #915 from tykeal/dependabot/github_actions/step-security/harden-runner-2.22.3
Chore: Bump step-security/harden-runner from 2.22.2 to 2.22.3
2026-11-05 14:48:41 -08:00
Andrew Grimberg aa1b0cfc4c Merge pull request #914 from tykeal/dependabot/github_actions/lfreleng-actions/generic-workflows/dot-github/workflows/semantic-pull-request.yaml-0.6.1
Chore: Bump lfreleng-actions/generic-workflows/.github/workflows/semantic-pull-request.yaml from 0.5.0 to 0.6.1
2026-11-05 14:48:37 -08:00
Andrew Grimberg ec54f45a76 Merge pull request #912 from tykeal/ci/816-ruff-complexity-gate
Build(ruff): Add complexity ratchet gate
2026-11-02 12:34:07 -08:00
Andrew GrimbergandCopilot b8eb514ee8 Build(ruff): Add complexity ratchet gate
Nothing stopped functions from growing back after the #816 splits.
Enable mccabe C901 and pylint PLR0912, PLR0913 and PLR0915 with each
limit set to the worst case measured in custom_components/ on the
merged tree, so the current code passes and any growth fails:

- C901 complexity: 16 (store_sync.py:208)
- PLR0912 branches: 15 (store_sync.py:208)
- PLR0913 arguments: 7 (checkin_protection.py, codegen.py,
  bookings_only.py)
- PLR0915 statements: 46 (coordinator_setup_shell.py:119)

tests/ also passes C901 (max 8) and PLR0912 (max 7), so those stay
gated there. PLR0913 (max 12, pytest fixtures) and PLR0915 (max 105,
integration scenarios) are ignored for tests/** because fixture
arguments and long scenarios are idiomatic and would only loosen the
production limits. PLR0913 does not conflict with Home Assistant
callback signatures at 7, so it stays enabled.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:26:47 -08:00
Andrew Grimberg 71c1a62318 Merge pull request #911 from tykeal/refactor/816-split-oversized-functions
Refactor(coordinator): Split reservation builder
2026-11-02 12:25:56 -08:00
Andrew GrimbergandCopilot 1efab15543 Refactor(coordinator): Split ghost reconstruction
_ghost_from_mapping interleaved defensive reads of corrupt persisted
fields, the physical-name fence, the stay-date fence and the
Reservation construction in one function.

Extract _dict_field and _str_field for the corrupt-tolerant reads
and _ghost_stay for the slot-name and stay-date fence. Each fence
still runs in the same order, logs the same message and returns None
on the same inputs, so ghost reservations are unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:19:32 -08:00
Andrew GrimbergandCopilot 517af0bdbc Refactor(allocator): Split resolve_cycle
resolve_cycle carried the whole refresh cycle inline: staging forced
re-issues, accounting for the observed lock, allocating every
requested identity and splicing rolled-back re-issues into the
outcome list.

Extract _stage_forced_reissues, _account_for_observation and
_allocate_cycle in issuance.py, and move the rollback splice to
reissue.replace_rolled_back beside rollback_blocked_reissues, which
also keeps issuance.py under the 441-line module gate. Every step
still runs under the allocator lock in the same order with the same
inputs, so guest-code allocation is unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:18:02 -08:00
Andrew GrimbergandCopilot 6916972689 Refactor(init): Split async_setup_entry
async_setup_entry mixed one-shot config cleanup, coordinator
construction, first refresh and platform setup, and duplicated its
rollback across separate CancelledError and Exception handlers.

Extract _consume_generate_flag, _async_prepare_coordinator and
_async_start_coordinator, and merge the identical rollback handlers
into a single except clause. Ordering of every setup step and the
rollback behaviour are unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:15:35 -08:00
Andrew GrimbergandCopilot 7018c00f96 Refactor(coordinator): Split reservation builder
_build_one_reservation resolved an event's stay, matched it to an
observed lock slot, chose between the generated and observed code,
and built the Reservation, all in one function of nearly a hundred
lines. Split those steps into _event_stay, _match_observed_slot,
_choose_code and _new_reservation, carrying the resolved stay and the
code decision in two small frozen dataclasses.

Behaviour is unchanged. The rotation origin is still recorded only
when a matched slot with a code must rotate. Alias and phone
extraction stay outside the ValueError guard that skips invalid
windows.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:15:34 -08:00
Andrew Grimberg 85623c9d51 Merge pull request #910 from tykeal/refactor/816-split-code-allocation
Refactor(allocator): Share slot notice scaffolding
2026-11-02 12:15:14 -08:00
Andrew GrimbergandCopilot 89ac473d24 Refactor(allocator): Decompose async_resolve_codes
async_resolve_codes ran every stage of the allocation step inline in
one function of over a hundred lines, unpacking the request into
locals along the way. Split it into named steps that read in cycle
order: re-validate exclusions, withhold when no allocator exists,
guard and rotate owned codes, build the gated cycle request, then
record the outcome and report exhaustion. The two reservation-clearing
loops move to cycle_results as withhold_codes and clear_unresolved.

Each step reads the request fields it needs. Only forced_reissues and
suppression can change after rotation, and the original already used
the rotated values for both, so every step sees the same inputs as
before. Side effects keep their original order, so behaviour is
unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:07:54 -08:00
Andrew GrimbergandCopilot 5d638f694f Refactor(allocator): Extract cycle result handling
Writing the allocator's adoption and allocation results back onto the
in-cycle reservations, including the origin bookkeeping and the masked
code_ref logging, is a separate concern from orchestrating the cycle.
Move those helpers from code_allocation.py into cycle_results.py and
expose the two entry points the allocation step calls.

Behaviour is unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:05:56 -08:00
Andrew GrimbergandCopilot e94a61543f Refactor(allocator): Extract cycle request builder
The functions that turn one entry's observed slots and reservations
into the allocator's cycle inputs (the observation, adoption,
allocation and rekey requests, the active identity set, planned slots
and the observed-slot matcher) made up nearly half of
code_allocation.py. Move them into cycle_requests.py so the allocation
step only orchestrates the cycle.

The observed-slot matcher becomes the public matched_code_slot, since
the rotation guard and divergence check receive it from the step.
build_cycle_observation is moved verbatim and stays unfiltered by
exclusions; the source-level test pinning that now reads the new
file. Importers in the allocator package and the tests now use
cycle_requests directly. Behaviour is unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:04:41 -08:00
Andrew GrimbergandCopilot 57b82a6792 Refactor(allocator): Move adoption gate predicate
code_allocation._adoption_complete was a thin boolean wrapper over
adoption_gate.evaluate, yet the forced re-issue preview reached into
code_allocation for it as a private name. Move it into adoption_gate
as the public is_complete predicate, beside the evaluation it wraps,
and point the preview and the tests at it there.

Behaviour is unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:02:53 -08:00
Andrew GrimbergandCopilot 1c4cc508e9 Refactor(allocator): Move exclusion revalidation
Re-deriving an entry's persisted slot exclusions from the cycle's
observation, logging every cleared exclusion and persisting the
survivors, is exclusion bookkeeping rather than code allocation. Move
it from code_allocation._revalidated_exclusions into slot_exclusions
as revalidate_persisted, next to the load, revalidate and persist
helpers it composes.

The allocation step calls it through the module attribute, so tests
patch slot_exclusions.revalidate_persisted where it is looked up.
Behaviour is unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:01:38 -08:00
Andrew GrimbergandCopilot c4849a7154 Refactor(allocator): Move unreadable slot notice
code_allocation.py mixed the per-cycle allocation step with the
operator notification for unreadable managed slots. Move the
unreadable-slot notification id, dismiss and report functions into
their own unreadable_slots module so the allocation step only decides
when to report, not how.

Importers, including entry teardown in __init__.py and the tests,
now import the functions from their new home. Behaviour is
unchanged.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 12:00:18 -08:00
Andrew GrimbergandCopilot 0dfbca1668 Refactor(allocator): Share slot notice scaffolding
The excluded-slot notification in slot_exclusions.py and the
unreadable-slot notification in code_allocation.py each carried an
identical trio: a per-entry notification id, a bus-guarded dismiss,
and a bus-guarded create with a fixed title. Extract that scaffolding
into a SlotNotice value in slot_notifications.py so both notices share
one implementation and only own their message text.

Behaviour is unchanged: ids, titles, messages and the no-bus guard are
identical. Tests now patch the notification platform where it is
looked up, in slot_notifications.

Refs #816

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 11:58:59 -08:00
Andrew Grimberg 2af037ee56 Merge pull request #909 from tykeal/fix/893-corrupt
Fix(allocator): Trust a rebuilt registry
2026-11-02 11:50:30 -08:00
Andrew GrimbergandCopilot 3e854f7d79 Fix(allocator): Regate entries with unread slots
The options listener changes an entry's lock or range in place, so an
adopted entry moved to another lock or widened kept counting as read
once the registry recovered, and other entries could be issued a code
sitting in a newly managed slot. Re-register such an entry, as a
reload would, so the adoption gate waits for a cycle to read it.

Part of #893 (#851)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 11:43:37 -08:00
Andrew GrimbergandCopilot 73839b7536 Fix(allocator): Save grants despite declines
A trusted rebuild cycle that declined one identity but issued another
code scheduled no save, so a restart before the lock was read back
lost the issued code. Save the trusted registry regardless, and keep
only the clearing of the loss flags conditional on nothing having
been declined; a declined generator-only identity stays bound to its
proven preference by continuity_required once the flags clear.

Part of #893 (#851)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 11:43:31 -08:00
Andrew GrimbergandCopilot 11bcb57d7b Fix(allocator): Save trusted allocations, renotify
A single allocation issued while a corrupt registry was trusted was
never saved, so a restart could lose it before a cycle persisted the
registry; save it, and clear the loss flags, as a sound cycle would.

An adopted lock returning to the gate after a notice had already gone
out for another entry left that notice unchanged, so the newly
unreachable lock was never named. Re-arm the notice when an entry
newly rejoins the pending set.

Part of #893 (#851)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 11:34:07 -08:00
Andrew GrimbergandCopilot d570bb007a Docs(release): Add note for registry rebuild
Part of #893 (#851)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 11:28:18 -08:00
Andrew GrimbergandCopilot ad3eea10be Fix(allocator): Trust a rebuilt registry
A corrupt (not missing) registry kept every unpublished request
withheld until a lockless cycle cleared the loss flags, so a site with
only lock-backed entries stayed wedged and an excluded slot could hold
recovery forever. Per the maintainer's decision, trust the rebuilt
registry once every configured lock-backed entry has positively
adopted its current lock and slot range, with operator-excluded slots
counting as observed. An unreachable lock still withholds issuance;
the adoption notification now says the registry is being rebuilt and
labels a lock none of whose slots can be read, and an adopted lock that
becomes unsound while the registry is lost returns to the gate so the
notification reappears. Diagnostics report entries whose lock or range
is not yet adopted.

Closes #893 (#851)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 11:28:17 -08:00
Andrew Grimberg 00117918c2 Merge pull request #908 from tykeal/fix/893-gate
Fix(allocator): Scope the gate to unread locks
2026-11-02 11:27:59 -08:00
Andrew GrimbergandCopilot 04acce86ea Fix(allocator): Recheck exclusions on read locks
A pending lock read with an excluded unreadable slot stayed trusted
after the operator removed the exclusion, so until its next cycle
other entries could be issued a code that slot might hold. Keep the
unreadable slots with the cached read and honour it only while the
entry's current exclusions still cover every one of them.

Part of #893 (#846)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:42:06 -08:00
Andrew GrimbergandCopilot 3f9a4f5e3e Fix(allocator): Bind read locks to their range
The options update listener changes an entry's lock or slot range in
place without re-registering it, so a read cached by entry id alone
kept freeing other entries after the pending entry moved to a lock or
range nobody had read. Store the observed lock and slot range with the
read codes and honour them only while the current configuration is
still covered, as adoption already does (#856).

Part of #893 (#846)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:36:38 -08:00
Andrew GrimbergandCopilot 1648da1a33 Docs(release): Add note for adoption gate scope
Part of #893 (#846).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:26:06 -08:00
Andrew GrimbergandCopilot fe4eba42ae Fix(allocator): Scope the gate to unread locks
The adoption gate was global: while any lock-backed entry waited for
adoption, no entry issued a code, so one entry stuck on a code that
matches no reservation withheld codes from every unrelated entry,
including generator-only ones (#846).

Every entry shares one door code space, so a lock whose codes are not
known must still withhold issuance from everyone: any code could
already sit in it. But a pending lock whose every managed slot was
read holds only known codes. Such an entry now withholds issuance for
itself alone, and the codes read from it that the registry does not
hold are excluded from selection for every other entry, in real
allocations and in re-issue previews. A lock not read in full since
its entry was registered, a lock with an unread slot, and a lost
registry all keep the gate global. Diagnostics list the pending
entries whose lock was read in full.

Part of #893 (#846).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:25:54 -08:00
Andrew Grimberg b83fc58b1e Merge pull request #907 from tykeal/fix/893-range
Fix(allocator): Bind adoption to the slot range
2026-11-02 10:16:08 -08:00
Andrew GrimbergandCopilot 0ac491fead Docs(release): Add note for slot range adoption
Part of #893 (#856).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:09:20 -08:00
Andrew GrimbergandCopilot 808f466243 Fix(allocator): Bind adoption to the slot range
Adoption was recorded against the lock a complete cycle read but not
against the slots it read. Widening or moving an entry's start_slot
or max_events brings slots under management that may hold codes the
registry never saw, yet the entry still counted as having observed
its lock, so lockless recovery after a registry loss could issue one
of those codes before a cycle read them (#856).

Record the lock and the managed slots a sound cycle read, and treat
an entry as observed only while its configured lock matches and its
configured range is covered by those slots. Narrowing the range
exposes nothing new and keeps the entry observed. Diagnostics now
list each entry's adopted lock and slots.

The pending-entry description moves from the allocator class into
recovery.py, beside the other gate policy, to keep allocator.py
within the file size limit.

Part of #893 (#856).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:09:05 -08:00
Andrew Grimberg ec324e225e Merge pull request #905 from tykeal/fix/893-origin
Fix(allocator): Harden registry-loss recovery
2026-11-02 10:05:06 -08:00
Andrew GrimbergandCopilot 09b989a80a Docs(release): Clarify origin recovery rule
Recovery depends on the recorded origin, not on when a code was
issued. Describe the first-refresh migration window and the
preferred-only recovery rule.

Part of #893.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:01:45 -08:00
Andrew GrimbergandCopilot 7ba0ec47c2 Fix(allocator): Clear codes before deleting entry
Deleting a disabled lock-backed entry releases the adoption gate but
leaves its codes programmed in the lock. With the registry lost
nothing records them, so following the notification's advice could
resume issuance of a code that still opens that lock. Tell the
operator to clear the entry's codes from the lock before deleting
it, matching the README.

Part of #893 (#854).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:01:45 -08:00
Andrew GrimbergandCopilot f6b4c0fc24 Fix(allocator): Keep lost lockless codes bound
Recovery flags clear on the first sound cycle of any entry. A
published generator-only booking missing from the rebuilt registry,
because it had not refreshed yet or because recovery refused it, then
fell back to normal issuance on its next cycle and could be handed
its free preference although its guest holds a collision-resolved
code. Bind such a booking to its proven preference on every cycle,
not only while the flags are set; a deliberate forced re-issue stays
exempt because the operator asked for a new code.

Also keep the recorded origin through a feed miss: ghosts are now
hydrated with it, and a cycle with no known preference keeps the
origin instead of erasing it. A malformed stored origin such as a
list is scrubbed to unknown instead of raising and discarding the
whole per-entry cache.

Part of #893 (#855).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:01:45 -08:00
Andrew GrimbergandCopilot 7b3e4dd3ab Docs(release): Add note for registry-loss fixes
Describe the recorded code origin, its upgrade effect on lockless
recovery, and the disabled lock entry gate.

Part of #893.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:01:44 -08:00
Andrew GrimbergandCopilot 9d8da1fb07 Fix(allocator): Gate on disabled lock entries
A disabled config entry runs no cycles, but disabling it clears no
lock slot, so its codes stay programmed. While the shared registry is
intact it still records those codes. Once the registry is lost or
corrupt nothing does, yet the adoption gate skipped disabled entries,
so other entries could issue a code that already opens that lock
(#854).

Enroll disabled lock-backed entries in the adoption gate when the
registry is lost, keep such an entry pending when it is unloaded
before it adopts, and release it only when it is enabled and adopts
or is deleted. The adoption-pending notification marks the entry as
disabled and says how to clear it. An intact registry keeps the old
behaviour, since it still accounts for the disabled lock's codes.

Part of #893 (#854).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:01:44 -08:00
Andrew GrimbergandCopilot 4c09e9d502 Fix(allocator): Record issued code origin
Lockless recovery after registry loss re-issued a previously
published code by re-deriving the preferred code for the booking.
A guest whose code was collision-resolved would silently get a
different code (#855). Persist the allocation origin (preferred or
collision_resolved) in the lockless store mappings and refuse
recovery for a previously published identity unless its recorded
origin is preferred, so continuity is proven rather than assumed.
A kept code is recorded as preferred only while it still equals the
generator's current preference, since the registry keeps handing an
identity its first code after that preference drifts.

Bump STORE_SCHEMA_VERSION to 2 with a migration that marks existing
mappings as having an unknown origin, which recovery treats as
unproven. The HA Store envelope version stays at 1 via a separate
STORE_FILE_VERSION constant, since changing it without a migrate
hook makes Store refuse to load the cached data.

Part of #893 (#855).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 10:01:44 -08:00
Andrew Grimberg fbab752a20 Merge pull request #906 from tykeal/docs/note-847
Docs(notes): Add release note for #847
2026-11-02 09:57:01 -08:00
Andrew GrimbergandCopilot 710e2c1f90 Docs(notes): Add release note for #847
The lockless door code fix from PR #901 merged before reno tooling
existed, so it has no release note. Add one so the fix appears in the
next release: checked-out stays on entries without a lock keep their
code through checkout day, and dateless checked-out stays no longer
claim a future stay by guest name alone.

Refs #847

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:43:37 -08:00
Andrew Grimberg f54629f414 Merge pull request #904 from tykeal/fix/amended-stay-rekey
Fix: Re-key adopted slots after a stay is amended
2026-11-02 09:41:36 -08:00
Andrew GrimbergandCopilot b198baac42 Fix: Never displace a mapped stay in the fallback
The amended-stay fallback could pick a reservation that already has
its own Store mapping, for example a calendar-written mapping for a
slot that was not observed this refresh. The rebuild then deleted that
mapping and put the stale adopted one in its place, losing its slot,
snapshot and history. The exact remap keeps its existing behaviour;
the name-only fallback now skips any reservation already in the
Store.

Refs #867

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:37:38 -08:00
Andrew GrimbergandCopilot 51f6029d1e Fix: Keep contested exact matches claimed
When two mappings both matched one reservation exactly, the uniqueness
filter dropped both claims before the amended-stay fallback ran. An
adopted slot with the same name but stale dates could then take that
reservation even though the exact matches conflict. Exclude every
exact target from the fallback, and apply uniqueness only to the
remaps that are kept.

Refs #867

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:37:38 -08:00
Andrew GrimbergandCopilot 63ac9c2837 Fix: Remap adopted slots on later refreshes
Fresh slot observations never carry a persisted identity key, and
adoption skipped any slot the Store already maps. An adopted mapping
saved on an earlier refresh was therefore missing from the observed
mapping keys: the physical remap never saw it and its snapshot was
never refreshed, so it became a ghost even when the lock matched a
current stay exactly. A stay amended after the adoption refresh hit
the same dead end.

Attach a readable coded slot to its existing adopted mapping, under
the same conditions that adopt a new one, so later refreshes treat it
exactly like the adoption refresh did.

Refs #867

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:37:38 -08:00
Andrew GrimbergandCopilot 6c59d02b67 Chore: Remove unused find_reservation_rematch
find_reservation_rematch() and its rematch_* helper modules have had
no production caller since the Store became the authority for slot
mappings. #868 deferred their removal because the UID-alias rule with
date_shifted=True looked like the fix for amended stays, and PR #898
kept the code for #867.

The #867 fix re-keys amended adopted slots inside the physical remap
instead: adopted mappings carry no UID aliases, so the rematcher could
not have matched them. Nothing uses the rematcher now, so remove it,
its exports and the tests that covered only it. The fingerprint
stability and date-shift code rotation tests that lived alongside the
rematch tests exercise production code and are kept.

Refs #867
Refs #868

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:37:38 -08:00
Andrew GrimbergandCopilot 80f4fce855 Fix: Re-key adopted slots after a stay is amended
An adopted mapping is written from the lock, so it carries the lock's
programmed window and no calendar UID. When a booking's dates change
upstream, the lock keeps the old window until it is reprogrammed. The
physical remap required the observed window to equal the reservation's
buffered window, so the mapping kept its adopted key and
build_ghost_reservations() turned it into a ghost holding the slot
alongside the live, amended stay.

The UID-alias rule in find_reservation_rematch() cannot help here:
adopted mappings have no UID aliases, and wiring the rematcher back in
was rejected in favour of the Store being the authority. Instead the
remap gains a second pass for adopted mappings that matched no
reservation exactly. Such a mapping follows the one current reservation
carrying the slot's physical name, provided the lock window is known,
no current reservation still owns that window (a renamed stay keeps its
slot), no exact match already claimed the target and no other mapping
claims it. Normal planning then keeps the slot and writes the amended
dates.

Calendar-written mappings are excluded: an amended stay keeps its key
through its UID, so an unmatched one means the stay left the feed and
must stay on the ghost path instead of passing its slot and PIN to the
same guest's next booking.

The remap moves to physical_remap.py so slot_matching stays under the
module size limit; both callers are updated. The adopted key prefix
becomes a shared constant.

Closes #867

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:37:38 -08:00
Andrew Grimberg 476f585985 Merge pull request #902 from tykeal/fix/842-observed-aliases
Fix(allocator): Correct shared-code owner handling
2026-11-02 09:12:28 -08:00
Andrew Grimberg 810a5d69da Merge pull request #903 from tykeal/refactor/probatio-imports
Refactor: Import probatio directly, not voluptuous
2026-11-02 09:10:56 -08:00
Andrew GrimbergandCopilot b3e59bdb27 Docs(release): Add note for shared-code fixes
Describe the #842 and #870 fixes for users, including the one-time
conflict notifications an upgrade may raise.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:07:40 -08:00
Andrew GrimbergandCopilot 1f7a1eb2a0 Fix(allocator): Judge formation over bookings
The formation report judged a shared code over every owner, so a
phoneless observed alias or forced-release hold beside two bookings
that share one guest's phone digits raised a false conflict, and the
verdict it cached disagreed with later re-judgement.

When two or more bookings share the code, only they are judged and
the verdict cached. A code shared by one booking and an alias or hold
is still one code presented by two lock slots, so it is announced as
it forms, as before, but not cached or re-judged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:07:33 -08:00
Andrew GrimbergandCopilot b16646eaa7 Fix(allocator): Re-judge shared codes each cycle
Shared-code conflicts were judged only on the edge where a new owner
joined a code. The registry outlives the refresh cycle, so a record
that was already shared was never judged again: a conflict silenced
at formation under #857's configuration-only rule kept that silence
after the #861 fix, and a record whose phone evidence later decayed
was never reported.

Every cycle now re-judges the shared records its entry holds, after
code_origin.record_cycle_evidence has published that cycle's phone
evidence. The last verdict (issuance owners and whether the sharing
is explained) is kept per code_ref and a report is raised only when
it changes, so a stable record is reported once instead of on every
refresh. Observed aliases take no part. A record is not judged while
an enabled owner entry has not yet run a cycle, which would otherwise
raise false reports during startup. Verdicts live in memory, so an
unexplained conflict is reported once on the first cycle after an
upgrade or restart. Diagnostics list the reported code_refs.

Closes #842
Closes #870

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:07:32 -08:00
Andrew GrimbergandCopilot 313c0f396f Fix(allocator): Keep observed aliases off owners
reserve_observed_code skipped a suppressed slot whose observed code
already had a registry record. When that record's owner was released
after its own slot cleared, the code looked free while the suppressed
slot still presented it, so a third booking could be issued a code
that opens that slot.

The alias is now recorded alongside any existing owner, unless an
owner already stands for the same physical slot. To make that safe,
observed aliases are no longer treated as issuance owners: the
release guard (via reissue._conflict_exempt) does not report an
adoption_conflict when only one booking actually holds the code, or
when the owner being released is itself an alias, and
AllocationRegistry.conflicts() counts issuance owners only. The alias
still keeps the code reserved until the lock stops presenting it, and
a record shared by two real bookings remains a conflict.

A forced re-issue of an aliased slot turns the alias into a
forced-release hold, so holds are not issuance owners either: the
hold's booking already has its replacement code. A hold is still
released only through its named forced-release exemption.

Part 1 of #842.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:07:32 -08:00
Andrew GrimbergandCopilot 1edb2a349b Refactor: Import probatio directly, not voluptuous
Home Assistant has validated schemas with Probatio since 2026.9;
'voluptuous' is now only a compatibility shim that core installs over
the real package at import time. Use 'import probatio' and
'probatio.X' in every shipped module, matching core's own style, so
the integration no longer depends on the shim surviving.

The shim aliases probatio's own classes, and Home Assistant's cv
validators raise probatio.Invalid, so schemas, error handling and
messages are unchanged. Probatio is a Home Assistant core
requirement, so it is not added to the manifest.

The guard test now also forbids importing voluptuous in shipped code,
and a new test pins that probatio.Invalid catches cv validator errors.

Closes #884

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 09:06:08 -08:00
Andrew Grimberg 0e92261401 Merge pull request #900 from tykeal/feat/reno-release-notes
Feat(release): Add reno release note snippets
2026-11-02 09:02:45 -08:00
Andrew GrimbergandCopilot 887eedb2c1 Fix(ci): Detect breaking titles in note check
The autolabeler adds the breaking-change label with GITHUB_TOKEN,
which does not trigger a new workflow run, so a 'Type!:' pull request
could pass the release note check unlabelled. Apply the autolabeler's
title rule directly and re-run on 'edited' events.

Also drop scripts/__init__.py: a top-level 'scripts' package in the
workspace shadows the one the Grype audit action runs with
'python -m scripts.collect_bypasses', which broke that job. The
renderer tests now load the script by path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 08:57:25 -08:00
Andrew Grimberg 95c314370b Merge pull request #901 from tykeal/fix/lockless-checked-out-codes
Fix: Keep checked-out codes on lockless entries
2026-11-02 08:57:24 -08:00
Andrew GrimbergandCopilot 236b03c11d Chore: Log when check-in marks a stay checked out
Marking a reservation checked out removes it from slot planning, and
on a lock-backed entry that blanks its sensor code, yet nothing was
logged. That silence made field reports of missing codes hard to
trace back to the check-in sensor. Log the transition at debug level
with the entry and the reservation's identity key.

Refs #847

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 08:49:58 -08:00
Andrew GrimbergandCopilot 617bee6227 Feat(release): Add reno release note snippets
The draft release body is rebuilt from .github/release-drafter.yml on
every push to main, so hand-written upgrade guidance was wiped by the
next merge and operator-facing changes had nowhere to live.

Adopt reno (dev dependency, in its own 'releasenotes' group so CI can
install it without Home Assistant). Notes live in releasenotes/notes/
and are written in Markdown. scripts/render_release_notes.py uses
reno's loader to select the notes added since the latest release tag
and renders them as Markdown, because 'reno report' only emits RST.

The release-drafter workflow now checks out full history, renders the
notes into a generated copy of the config and loads it with the file:
scheme. The downloads badge moves from 'template' to 'header' so the
notes land between the badge and the changelog categories; with no
notes the body is byte-for-byte what it was before.

A new Release Notes PR check runs 'reno lint' when releasenotes/
changes and fails a 'breaking-change' pull request that adds no note.
The first note covers the HA 2026.11 calendar trigger offset change
and Marketplace readiness for v3.10.0.

Closes #819

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 08:49:43 -08:00
Andrew GrimbergandCopilot 369fe7b833 Fix: Stop dateless checkouts claiming future stays
When the check-in sensor reports checked_out without start and end
attributes, select_checkin_match fell back to any uniquely named
reservation, including one that has not started. A stale sensor
could therefore flag an upcoming booking as checked out and keep it
out of slot planning, so it never received a slot or a code.

The name-only fallback now takes a started_by time and ignores stays
that begin after it. The coordinator passes the current time only
for the checked_out state; checked_in matching is unchanged. The
T043-3 test pinned the old behaviour with a future stay and now uses
a stay already in progress.

Refs #847

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 08:49:43 -08:00
Andrew GrimbergandCopilot 095b4eb6ee Fix: Keep checked-out codes on lockless entries
A stay that the check-in sensor reports as checked out is dropped
from slot planning so a lock-backed entry frees its slot. The
lockless path reused that planning filter for its allocator active
set, its allocation requests and its published reservation map, so
on an entry without a lock the allocator released the stay's code
and the event sensor blanked slot_code while the event was still
listed for the rest of the checkout day.

An entry without a lock has no slot to free, so checkout should not
change what it publishes. select_eligible_reservations gains an
include_checked_out flag that the lockless path sets. Ghosts that
have missed three refreshes stay excluded, so a stay that leaves the
feed is still pruned from the store as before. The lock-backed path
is unchanged: its sensors publish only codes observed on the lock.

The README contract now states that slot_code stays published while
the event is listed, including after checkout.

Closes #847

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 08:47:31 -08:00
Andrew Grimberg 0653b1904c Merge pull request #899 from tykeal/refactor/entity-id-inplace
Refactor: Opt in to in-place entity_id changes
2026-11-02 06:18:19 -08:00
Andrew GrimbergandCopilot 55c8056b34 Refactor: Opt in to in-place entity_id changes
Home Assistant 2026.11 renames an entity in place and calls
async_entity_id_changed instead of removing and re-adding it. Custom
classes that implement async_added_to_hass or
async_will_remove_from_hass keep the old remove/re-add fallback until
they opt in, and that fallback goes away in 2027.11.

Our two switches, the event sensor and the check-in sensor implement
these hooks. Nothing in them is keyed on their own entity_id: the
switches store object references, the check-in timer is time-based
and entity_id is only read at call time. So each class now defines
async_entity_id_changed and only chains to the parent hook.

HA only counts the hook when it is defined on the class that owns the
add/remove hooks, or on a subclass of it. A mixin earlier in the MRO
is a sibling and would not opt in, so each class defines the method
itself and delegates to a shared helper. The helper guards the parent
lookup because 2026.10 lacks the hook and never calls it.

Renaming in place also keeps the check-in sensor's pending timer,
which the remove/re-add fallback cancels.

Closes #890

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-11-02 06:12:57 -08:00
Andrew Grimberg f00030baae Merge pull request #878 from tykeal/dependabot/github_actions/lfreleng-actions/repository-metadata-action-0.3.3
Chore: Bump lfreleng-actions/repository-metadata-action
2026-11-02 06:04:14 -08:00
Andrew Grimberg 4efc157f91 Merge pull request #882 from tykeal/dependabot/pre_commit/pre-commit-hooks-e6767187a8
Chore: Bump https://github.com/astral-sh/ruff-pre-commit from v0.17.1 to 0.17.2 in the pre-commit-hooks group
2026-11-01 18:29:31 -08:00
Andrew Grimberg 38e0cd841b Merge pull request #881 from tykeal/dependabot/github_actions/step-security/harden-runner-2.22.2
Chore: Bump step-security/harden-runner from 2.22.1 to 2.22.2
2026-11-01 18:29:28 -08:00
Andrew Grimberg fbcd295142 Merge pull request #880 from tykeal/dependabot/github_actions/lfreleng-actions/security-workflows/dot-github/workflows/codeql.yaml-0.9.5
Chore: Bump lfreleng-actions/security-workflows/.github/workflows/codeql.yaml from 0.9.4 to 0.9.5
2026-11-01 18:29:25 -08:00
Andrew Grimberg 2773c078b1 Merge pull request #879 from tykeal/dependabot/github_actions/lfreleng-actions/generic-workflows/dot-github/workflows/semantic-pull-request.yaml-0.5.0
Chore: Bump lfreleng-actions/generic-workflows/.github/workflows/semantic-pull-request.yaml from 0.4.3 to 0.5.0
2026-11-01 18:29:22 -08:00
Andrew Grimberg d1d91afbdc Merge pull request #875 from tykeal/dependabot/github_actions/lfreleng-actions/python-sbom-action-0.1.4
Chore: Bump lfreleng-actions/python-sbom-action from 0.1.3 to 0.1.4
2026-11-01 18:29:16 -08:00
Andrew Grimberg d8d7c616ad Merge pull request #877 from tykeal/dependabot/github_actions/home-assistant/actions/hassfest-86d4564712fa02543c808e3a693dd04f9d3f8361
Chore: Bump home-assistant/actions/hassfest from cd6f3635a740c84d8a14f6121a90068b8ece6bf6 to 86d4564712fa02543c808e3a693dd04f9d3f8361
2026-11-01 18:29:12 -08:00
Andrew Grimberg e680f87318 Merge pull request #873 from tykeal/dependabot/github_actions/lfreleng-actions/security-workflows/dot-github/workflows/openssf-scorecard.yaml-0.9.5
Chore: Bump lfreleng-actions/security-workflows/.github/workflows/openssf-scorecard.yaml from 0.9.4 to 0.9.5
2026-11-01 18:29:08 -08:00
Andrew Grimberg 814e37d98a Merge pull request #876 from tykeal/dependabot/github_actions/lfreleng-actions/release-assets-action-0.1.4
Chore: Bump lfreleng-actions/release-assets-action from 0.1.3 to 0.1.4
2026-11-01 18:29:04 -08:00
Andrew Grimberg b8c401c406 Merge pull request #874 from tykeal/dependabot/github_actions/lfreleng-actions/draft-release-promote-action-0.1.7
Chore: Bump lfreleng-actions/draft-release-promote-action from 0.1.6 to 0.1.7
2026-11-01 18:29:01 -08:00
Andrew Grimberg b08762e8a9 Merge pull request #898 from tykeal/chore/remove-dead-adoption-bootstrap
Chore: Remove dead adoption bootstrap code
2026-10-30 14:26:04 -07:00
Andrew GrimbergandCopilot c244554964 Chore: Remove dead adoption bootstrap code
The first-upgrade adoption entry point async_adopt_keymaster_slots,
its _async_adopt_keymaster_slots_impl shell, and the pure
plan_adoption/_adoption_mapping helpers had no production caller.
Adoption of coded slots without a Store mapping now happens on every
refresh through _adopt_observed_coded_slots, so the old bootstrap
path only survived in tests. AdoptionMappingDecision existed solely
for plan_adoption and is removed with it.

Tests that exercised only the dead path are removed. Two regression
tests that used the old entry point merely to seed adopted mappings
now seed them through the live _adopt_observed_coded_slots path.

find_reservation_rematch is intentionally kept; its future is
deferred to #867.

Closes #868

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 14:21:19 -07:00
Andrew Grimberg f803834bf1 Merge pull request #897 from tykeal/docs/nolock-contract
Docs(readme): Document no-lock sensor contract
2026-10-30 14:20:47 -07:00
Andrew Grimberg 7c806ac014 Merge pull request #896 from tykeal/feat/reconciliation-failure-signal
Feat(coordinator): Report reconciliation failures
2026-10-30 14:18:41 -07:00
Andrew GrimbergandCopilot fd71d1e2d0 Docs(readme): Clarify no-lock retention and keys
Review found three gaps in the lockless contract section. The
calendar entity's current-event attributes advance at the stay's end,
so only its event list shares the checkout-day retention guarantee.
The example automation reacted only to slot_code, missing amendments
that keep the code but move the dates. And slot_name alone is not a
unique reservation key, since two stays can share a guest name.

Scope the retention clause to the event sensors and the calendar
event list, trigger the sketch on start and end as well, and
recommend keying lock actions on uid or on name plus dates.

Refs #872

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 14:14:11 -07:00
Andrew GrimbergandCopilot 835abfcc2e Docs(readme): Document no-lock sensor contract
Entries without a Keymaster lock are now used to drive locks that
Keymaster cannot, such as TTLock, through automations that read the
event sensor attributes. For those users the attributes are the only
interface to the integration, yet nothing stated what they promise.

Add a "Using Door Codes Without Keymaster" section covering the
attributes and their formats, when slot_code is present or absent,
that an empty slot_code is absence of information rather than a
removal instruction, code and slot name stability, and retention of
an event through its whole checkout day. Warn that a late checkout
plus a deletion delay can cross midnight, after which the event is
gone, and include an example automation sketch.

Closes #872

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 14:08:46 -07:00
Andrew GrimbergandCopilot e88a06a725 Test(coordinator): Pin checkout-day retention
Lockless consumers delete door codes from automations that fire hours
after a stay ends, so an event must stay published for its whole
checkout day. That holds only because the calendar fetch window opens
at local midnight rather than now(); nothing in the suite guarded it,
and swapping in now() would silently break downstream code deletion.

Drive the real fetch and parse path under frozen time with a 12:00
checkout: the event is present at 08:00, 15:00 and 23:59 on checkout
day and gone at 00:30 the next day. Also assert the parser receives
start-of-local-day as from_date. Changing the window start to now()
fails three of these tests.

Refs #872

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 14:07:12 -07:00
Andrew GrimbergandCopilot 61739e60f0 Fix(coordinator): Warn of partial reconciliation
async_apply_plan applies actions in sequence without rollback, so a
reconciliation failure can arrive after some slots were already
programmed or cleared. The notification claimed the lock kept the
last successful cycle's codes; say instead that the update may be
partial and ask the operator to check the codes on the lock.

Refs #852

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 14:04:44 -07:00
Andrew GrimbergandCopilot 8bed71b091 Feat(coordinator): Report reconciliation failures
_run_reconciliation only logged its traceback, so a lock-backed entry
whose reconciliation failed every cycle silently stopped programming
its lock. #850 added user-facing reporting for the lockless path
only.

Generalise allocation_failure with a FailureKind, keyed per kind and
entry, and wire reconciliation into it: a failure raises a
throttled persistent notification with its own id
(rental_control_reconciliation_failed_<entry>), a successful cycle
resolves it, and diagnostics gain a reconciliation_failure block.
The lockless kind keeps its existing id, text and diagnostics, so
the two kinds never overwrite each other.

forget_entry now clears every kind, so unload and removal clean up
both. Switching between lockless and lock-backed modes drops the
other mode's open report, because that mode's cycle no longer runs
to resolve it.

The notification text is English-only, matching the existing
lockless notification, which does not use translations.

Closes #852

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 14:00:48 -07:00
Andrew Grimberg 297d6abb28 Merge pull request #894 from tykeal/feat/marketplace-readiness
Feat: Prepare for the Home Assistant Marketplace
2026-10-30 14:00:27 -07:00
Andrew Grimberg 70f5b2918b Merge pull request #895 from tykeal/fix/none-lock-sentinel
Fix(coordinator): Treat "(none)" lock consistently
2026-10-30 13:57:23 -07:00
Andrew GrimbergandCopilot cb77debb14 Docs: Rewrite installation for HA Marketplace
Home Assistant 2026.11 replaces HACS with a built-in Marketplace, and
Rental Control is already in its default catalog. The Installation
section still documented a manual copy into custom_components, told
users to install HACS and restart, and called Keymaster a hard
dependency.

Make the Marketplace the primary path: install from Settings >
Marketplace and continue straight to Add Integration with no restart.
Keep the hacs_repository My-link badge, which opens HACS before 2026.11
and the Marketplace from 2026.11 on, and fix its category to the
lowercase integration value. Note that users on older releases install
through HACS and still restart before setup. Drop the manual
installation subsection.

Describe Keymaster as optional: it is needed only to program lock
slots, while calendars, sensors and door codes work without it. Add a
note on the 2026.11 calendar trigger change, where the offset sign now
sets the direction, so users check YAML automations that use an offset
without offset_type. Update the Table of Contents to match.

Closes #886

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 13:56:35 -07:00
Andrew GrimbergandCopilot 336a80172d Refactor: Construct DeviceInfo instead of dict
In Home Assistant 2026.11 DeviceInfo becomes a dataclass instead of a
TypedDict, and the dict-style compatibility layer is slated for
deprecation and removal. Build the coordinator's device info with the
DeviceInfo constructor rather than a dict literal. The constructor
call works on the 2026.10 floor, where DeviceInfo is a callable
TypedDict, and on 2026.11+, so the minimum version need not move.

No production code, diagnostics or tests read device info with
dict-style access, so nothing else needs to change.

Closes #889

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 13:56:34 -07:00
Andrew GrimbergandCopilot 1e39b0628e Feat: Ship brand images in integration folder
Since Home Assistant 2026.3 a custom integration can ship its own
brand images in custom_components/<domain>/brand/. Local images take
precedence over the brands repository, and the 2026.11 Marketplace
shows them on the project page before install. The HACS action
currently warns that they are missing and falls back to the brands
repository.

Copy icon.png, icon@2x.png, logo.png and logo@2x.png unchanged from
custom_integrations/rental_control/ in home-assistant/brands. They
are the project's own logo, so license them under the project's
copyright and Apache-2.0 via .license sidecars, matching how
manifest.json and strings.json are licensed. The release workflow
zips the whole integration directory, so brand/ ships in
rental_control.zip without workflow changes.

Closes #888

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-30 13:56:34 -07:00