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>
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>
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>
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>
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>
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>
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>
_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>
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>
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>
_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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>