2068 Commits
Author SHA1 Message Date
7333104b4f Fix(coordinator): Clear reports on lock switch
An options update that gives a generator-only entry a lock
reconfigures the coordinator in place, so later refreshes run
reconciliation and never reach the lockless resolve call. A failure
report raised before the switch therefore stayed in notifications and
diagnostics indefinitely, describing a condition that no longer
applies and that nothing could clear.

Drop any open report as a lock-backed refresh begins.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 09:52:24 -07:00
c318847e48 Fix(allocator): Re-adopt a reloaded lock entry
Unloading an entry clears its pending adoption gate, and reloading it
re-registers it, but its earlier membership of the adopted set
survived. The unobserved-lock safeguard therefore saw nothing to wait
for while the reloaded lock was in fact unread, so an empty lockless
cycle could clear the recovery flags; a subsequent setup failure then
removed the pending gate too, leaving normal issuance free to hand out
a code the lock still holds.

Withdraw adopted standing on registration, and require an empty
pending gate before the recovery flags may clear.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 09:51:36 -07:00
57f57d55de Fix(allocator): Detect locks in read-only data
Home Assistant exposes ConfigEntry.data as a MappingProxyType, which
is not a dict, so the isinstance(data, dict) guard made every real
config entry look lockless. That silently emptied both the adoption
gate seeded at allocator construction and the unobserved-lock set the
no-lock recovery path depends on, so recovery could issue a code that
a disabled or failed-setup lock still holds.

Accept any Mapping instead, and build the recovery fixtures from
read-only mappings so the regressions exercise the real type.

The diagnostics allocator fixture now stands in for the adoption cycle
its lock-backed entry needs, because the gate correctly holds that
entry once lock detection works.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 09:40:37 -07:00
403579227e Fix(allocator): Gate the missing-registry clear
Clearing the recovery flags returns the allocator to normal issuance,
where a code still programmed in a lock nobody has read looks free. The
missing-registry branch cleared them on any sound cycle, and an empty
lockless cycle is trivially sound because it declines nothing. A later
published reservation could then be handed a code from an unobserved
lock.

Require that every configured lock-backed entry has actually adopted
before either branch may clear, so the corrupt and missing cases carry
the same guarantee.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 09:30:35 -07:00
70a7c865de Fix(allocator): Prove adoption before recovering
Review found two further ways a code could be issued that no guest had
been given.

Filtering unobserved locks on disabled_by missed an enabled entry whose
setup failed: async_setup_entry unregisters it from the adoption gate
on failure, so neither the gate nor the filter saw it while its codes
stayed programmed in the lock. Track adoption positively instead, from
cycles that actually completed it, and require every configured
lock-backed entry to appear there.

Separately, the recovery flags are global while a decline is per entry,
so a clean cycle for one entry could clear them while another still had
a published reservation whose code was lost. Normal issuance would then
accept that reservation's all-zero placeholder. Withhold a published
reservation whose code is unknown regardless of the flags, and count
that decline when deciding whether the cycle may clear them.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 09:21:24 -07:00
34b969fe25 Fix(coordinator): Count failures across conditions
The consecutive-failure streak counts refresh cycles that failed, so
only a successful cycle should reset it. Keying the reset on the
exception message meant nine failures followed by a different error
reported a streak of one, hid how long the entry had been failing,
delayed every escalation threshold, and left recovery logging only the
final condition's count.

Keep the streak running until resolve() clears it, and announce a
changed condition on its own merits rather than by restarting the
count, so the operator still sees the new error immediately.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 09:10:06 -07:00
7457207979 Fix(allocator): Harden no-lock recovery gates
Review of the recovery path found three ways it could publish a code
no guest had ever been given.

A ghost reservation retained through a feed miss has no code, and
build_allocation_requests substitutes an all-zero placeholder for the
missing preference. Recovery would allocate that placeholder, and the
existing-owner shortcut would then keep the fabricated code when the
live booking returned. Carry whether the preference is real on the
request and refuse recovery without one.

A disabled lock-backed entry is never enrolled in the adoption gate,
yet unloading it leaves its codes in the lock. An empty pending set
therefore does not prove every physical code is in the registry, so
refuse recovery and hold the loss flags while one is configured.

A collision fallback during recovery is by definition not the code the
guest holds, so restrict recovery to the preferred code alone.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 09:10:00 -07:00
d4ce900b8b Test: Reset allocation failure state globally
The allocation failure reporter keeps its throttle state in a
module-level dict, exactly as the adoption gate does. The reset
fixture only lived in the reporter's own test module, so any other
test that drove a lockless allocation failure left an entry recorded
and could change what a later test observed.

Move the reset alongside the adoption gate's in tests/conftest.py so
it applies to every test, and reduce the local fixture to what is
actually local to it: capturing notifications.

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 08:48:36 -07:00
eadf23b519 Test(integration): Cover no-lock entries e2e
Nothing drove a generator-only config entry through setup, refresh and
sensor publication, so no test asserted the single thing such a user
observes: a non-empty slot_code on an event sensor. That gap is why an
allocator that silently withheld codes shipped.

Add an integration test that builds a no-lock entry with a date-based
generator, sets it up against a frozen clock and a stubbed calendar,
and asserts the event_0 sensor carries exactly the code the booking
dates imply, for both shapes a cleared lock picker persists as. Also
assert the code survives a second refresh, and that it survives a loss
of the shared registry, which is the user-visible shape of #845.

Closes #849

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 08:33:09 -07:00
189ffd4428 Feat(coordinator): Report allocation failures
When the lockless allocation step raised, the traceback reached the
log and nothing else. Every sensor on the entry then served the
previous cycle's codes, or no code at all, and the same failure
recurred on every refresh with no counter and no escalation, so a
user who does not read the log had nothing to attribute it to.

Add a persistent notification throttled by the failure's signature,
following the adoption gate's established pattern: one notification
per distinct condition, re-announced only at 10, 100 and 1000
consecutive failures, dismissed as soon as a cycle succeeds, and
dropped on entry unload or removal. The failure run is also exported
in the entry's diagnostics payload.

Reservations from the last successful cycle are deliberately kept
rather than cleared. Blanking every guest's code over a failure that
may be transient is the same user-visible harm this reporting exists
to explain, so the failure is surfaced instead of the codes being
withdrawn.

Closes #848

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 08:29:07 -07:00
00b4b923af Fix(allocator): Let no-lock entries recover codes
A generator-only config entry drives no lock, so it observes no slots
and has nothing to adopt. After a registry loss the fail-closed guard
declined every previously published reservation, and the flag-clearing
branch refused to run while any decline had happened, so the entry
declined the same reservations on every subsequent cycle. The user
lost every door code on that entry permanently and silently.

Allow recovery only when the whole cycle is lockless, the request
claims no physical slot, and no entry is still pending its first
adoption. That last condition means every lock-backed entry has
already re-registered the codes its locks hold, so recovery cannot
hand out a code that is programmed into a real lock. The code
re-issued is the entry's deterministic generated preferred code, so a
guest who already has a code keeps it.

Lock-backed entries, and bare allocation requests outside a cycle,
still fail closed exactly as before.

Closes #845

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 08:23:48 -07:00
Andrew Grimberg eeb6f58157 Merge pull request #844 from tykeal/fix/wrong-length-code-reservation
Fix(allocator): Reserve wrong-length lock codes
2026-10-23 06:23:43 -07:00
Andrew GrimbergandClaude 6019e81aa2 Fix(allocator): Reserve wrong-length lock codes
A managed slot whose observed code is not well formed for the owning
entry is suppressed from adoption, and the invalid-code branch recorded
nothing about the code the lock still holds. Lock slots are shared
across config entries and entries may use different code lengths, so a
4-digit entry observing a 5-digit code left those digits free for a
5-digit entry on the same lock to issue, programming a duplicate.

Reserve the observed code from that branch too, at its own length, so
it lands in the code space that can actually issue it. The existing
decimal guard in the helper keeps non-decimal codes skipped outright
because the allocator can never produce one. The slot is still added to
suppressed_slots so the adoption gate does not deadlock.

Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-23 06:16:40 -07:00
Andrew Grimberg 2ef5007339 Merge pull request #841 from tykeal/fix/suppressed-slot-code-reservation
Fix: Reserve codes held by suppressed lock slots
2026-10-23 06:12:16 -07:00
Andrew GrimbergandClaude 5c5424e478 Fix(allocator): Drop aliases adoption makes stale
Observed-code placeholders stand for a code a booking holds physically
but does not own. Two of them could still survive an adoption and sit
on one record as a second owner, which every later sweep reports as an
adoption conflict and which keeps the code from being reclaimed.

The mismatch path records a placeholder under the current fingerprint
without clearing one left by an earlier fingerprint of the same slot.
The ordinary path only looked at the slot being adopted, so a code that
moved to another slot left its old placeholder behind.

Clear an earlier fingerprint's placeholder for the slot on the mismatch
path, and on the ordinary path clear every placeholder of the booking
that names the adopted code wherever it sits. Neither loses a hold: the
new owner is retained while the lock still reports its code.

Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 16:17:34 -07:00
Andrew GrimbergandClaude 00dad835a5 Fix(allocator): Clear stale fingerprint aliases
A suppressed slot reserves its observed code under the identity of the
moment. If the booking's fingerprint changes before the gate opens, the
historical identity holds only that placeholder and no real allocation,
so the re-key path skips it and leaves the placeholder behind.

Adoption then adds the current identity to the same record, which reads
as an adoption conflict on every later sweep and keeps the code from
being reclaimed once the lock stops reporting it.

Sweep this slot's placeholders for the booking's earlier fingerprints
as well as its current one when adoption takes the slot over.

Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 16:08:58 -07:00
Andrew GrimbergandClaude 12df8a642f Fix(allocator): Supersede the observed placeholder
A suppressed slot's observed-code reservation is a placeholder for a
code the identity holds physically but does not own. When the gate
withholds the replacement allocation, the identity owns nothing, so a
later cycle adopts that unchanged code through the ordinary path.

Adoption only dropped the slot's observed alias when it named a
different code, so the placeholder and the new owner ended up on one
record. Every later release guard then reported an adoption conflict,
which retained the code for good and raised a conflict notification
that describes one identity colliding with its own placeholder.

Drop the slot's alias whenever that slot's identity adopts, because
adoption records the same physical fact the placeholder stood in for.

Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 16:01:05 -07:00
Andrew GrimbergandClaude aa7a8fea90 Fix(allocator): Reserve suppressed slots' codes
A slot that the adoption gate suppresses keeps whatever code is
physically programmed in it, but nothing recorded that code in the
shared allocation registry. Issuance decides availability purely from
the registry, so the allocator was free to hand the same code to
another booking and program a duplicate onto the lock. The registry is
shared across config entries, so the duplicate could cross entries
that share a lock.

Reserve the observed code under the existing observed-alias identity
whenever a suppression or rotation-pending branch declines adoption,
at the code's own length rather than the observing entry's configured
length, so a mixed-length peer entry is covered too. The reservation
is assigned to no reservation identity and is released by the normal
sweep once the lock stops reporting the code.

A code that already has a registry record is left alone: the record
already keeps it out of issuance, and adding an owner would raise a
spurious adoption conflict. Non-decimal codes are skipped because the
allocator can never produce one, so they cannot collide.

Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 15:51:02 -07:00
Andrew Grimberg 01742dc8d5 Merge pull request #840 from tykeal/fix/adoption-gate-diagnostics
Fix(allocator): Log why issuance is withheld
2026-10-22 15:39:25 -07:00
Andrew Grimberg 1dd568d044 Merge pull request #839 from tykeal/feat/diagnostics-platform
Feat(diagnostics): Add config entry diagnostics
2026-10-22 15:08:43 -07:00
Andrew GrimbergandCopilot 7f9af1e122 Test(diagnostics): Pin the masker salt in tests
The privacy tests assert that literals such as 2026 and 2223 never
appear anywhere in the serialized payload, but the identity tokens are
salted with fresh randomness on every download. A token digest can
contain either literal by chance, which would fail CI without any date
or PIN having leaked.

Pin the salt for this module so the tokens are deterministic, and
assert the expected tokens directly. The substring and timing
equivalence assertions are unchanged.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:59:58 -07:00
Andrew GrimbergandCopilot fab2514bac Fix(diagnostics): Drop timing and exclusion notes
Copilot review found two more ways the payload could carry a secret.

Slot exclusion reasons are free-form text supplied through
set_slot_exclusion, so a note naming a guest or quoting a PIN reached
both the entry exclusion list and the allocator snapshot, which also
carries other entries' exclusions. Report only whether a reason exists
and redact the text wherever it appears.

Window durations were still enough to rebuild a date-based code: an
active one-night stay combined with the exported check-in and checkout
times pins both dates, and the slot duration and protected_active
flags leak the same signal. Replace every duration with a comparison
of the observed slot window against the desired window, which answers
the question that matters during triage without exporting timings.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:50:57 -07:00
Andrew GrimbergandCopilot 5250532190 Fix(diagnostics): Hide code-generator seed inputs
Copilot review found that the payload still let a reader rebuild an
issued door code without ever seeing one.

generate_date_based_code derives the whole code from the start date,
the end date and the code length, and the allocator keeps that code
when it is free. Exporting the reservation and observed slot dates
beside the code length therefore exported the code, and the exported
buffer settings recovered the unbuffered dates as well. Report window
durations and a past/active/future marker instead.

candidate_codes derives its collision-resolution walk from the
identity key and the code length alone, and generate_static_random_code
seeds on the calendar UID, so an identity key is a code seed rather
than a mere join key. Replace identity keys with per-download salted
tokens applied across allocator owners, reconciliation keys and
values, slot rows and reservation rows, which keeps every join inside
one download while exporting no seed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:42:40 -07:00
Andrew GrimbergandCopilot 6a6b4b80f7 Docs(allocator): Scope the cross-entry wait
The reopen message said issuance waits on every entry's gate, but the
allocator only holds issuance while an entry has never completed its
first adoption cycle; resolve_cycle discards an entry from
_pending_adoption and never re-adds it. An operator could have waited
on an unrelated entry that was never blocking them.

Scope the wait to entries still awaiting their first adoption.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:40:19 -07:00
Andrew Grimberg e838de62c3 Merge pull request #838 from tykeal/ci/grype-scan-action
CI(grype): Adopt lfreleng grype-scan-action
2026-10-22 14:39:36 -07:00
Andrew GrimbergandCopilot ffb905d494 Docs(allocator): Clarify the gate reopen log line
The recovery line claimed issuance resumed, but it only observes this
entry's own adoption gate. Other entries may still be pending in the
shared allocator, which keeps rejecting allocations, so the line could
promise codes that do not arrive.

Say the entry's gate reopened and that issuance waits on every entry.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:34:12 -07:00
Andrew GrimbergandCopilot e7298abaa8 Fix(diagnostics): Redact error text and aliases
Copilot review found three leaks in the new diagnostics payload.

Failed Keymaster service calls store str(exc) as the slot error, so a
driver exception that quotes the submitted PIN or the guest name would
reach slots[].last_error and the reconciliation last_error and
last_slot_errors fields. Report only whether a slot has a recorded
failure and redact the message text.

The reconciliation projection also carries uid_aliases and
booking_aliases. Those are booking confirmation identifiers, and for
static_random the calendar UID is the code seed, so publishing it next
to the code length would let a reader regenerate the door code.

Finally, _observe_managed_slots classifies fresh physical state only
and never populates the identity or retry fields, so the slot rows
reported None and 0 as though they were observed facts. Source them
from the latest plan, the persisted Store mappings and the
EventOverrides retry counters instead.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:33:25 -07:00
Andrew GrimbergandCopilot bbef65f8a8 CI(grype): Drop redundant blanket ignores
The scan no longer passes a Grype config file, so .grype.yaml had
stopped being read at all. Delete it rather than leave a dead file
that reads as if it were still suppressing something.

Its four blanket ignores covered pillow, uv, PyJWT and cryptography.
Provenance classification now handles those structurally: each is
inherited through Home Assistant, absent from the declared list, and
so reported as a warning instead of gating. Confirmed on CI, which
classified all 39 current findings as inherited from the declared
list, and locally against the same SBOM, where synthetic findings in
icalendar, x-wr-timezone and homeassistant still gate at 'medium'.

Classification is also the better instrument. A blanket ignore drops
the finding outright, so nobody sees it; a warning keeps it in the
step summary and the SARIF report, where it can be read when Home
Assistant next moves its pins.

Not passing a config file at all removes something the action warns
to treat as untrusted when it arrives from a pull request head.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:30:22 -07:00
Andrew GrimbergandCopilot 234e93975d CI(grype): Adopt lfreleng grype-scan-action
Replace the hand-rolled Grype invocation in build-test.yaml and
release.yaml with lfreleng-actions/grype-scan-action, configured to
gate only on packages this project declares.

Every Grype finding this repository has ever had - pillow, uv, PyJWT,
cryptography - reaches the SBOM transitively through Home Assistant,
which pins its requirements exactly. We cannot bump them from here, so
the gate was a permanently red check that had to be triaged and
suppressed by hand on every upstream bump. That trains maintainers to
ignore the signal, which is the state in which a finding that genuinely
matters gets missed.

dependency-policy 'declared' keeps our own dependencies at the existing
'medium' bar and reports inherited ones as warnings. transitive-fail-on
stays at 'none', but as a decision rather than a default taken on
trust: 'critical' was tried first and immediately gated on five
criticals in Home Assistant's pinned pillow and uv, every one with an
upstream fix we cannot reach. Inherited findings still appear in the
step summary and the SARIF report.

The action would rather read the declared set out of the SBOM's
dependency graph, but it cannot here. cyclonedx-py scans the
environment rather than the project, so it emits no
'metadata.component' root, and the action refuses an unrooted graph.
An unusable source leaves every finding classified as declared, which
gates - strictly worse than today. A declared-dependencies list under
.github/ supplies the answer directly instead, as the action documents
for this case; it must be kept in step with pyproject.toml.

Artefact upload, step summary and the NO_BLOCK_AUDIT_FAIL override are
all preserved through the action's own inputs, as is the 'Grype Audit
SBOM' check name.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:30:09 -07:00
Andrew GrimbergandCopilot 43f439108f Fix(allocator): Log a missing door code allocator
When the shared allocator is unavailable, the allocation step clears
every reservation code and returns without saying anything. The branch
is defensive and should be unreachable, but it fails exactly as badly
as the adoption gate did: no codes anywhere, and nothing in the log to
explain it.

Log the branch at error level so an impossible failure is still a
visible one.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:28:50 -07:00
Andrew GrimbergandCopilot 47675d66c9 Fix(allocator): Log why issuance is withheld
The adoption gate withholds door code issuance for an entire config
entry, but only the unreadable-slot condition said so. A readable
managed slot holding a code that matches no current reservation shut
the gate with no log line and no notification at all, so door codes
simply stopped appearing with nothing to explain it. Triaging #830
meant asking a reporter to grep for warnings that may never have been
emitted.

Move the gate evaluation into a dedicated helper that returns the
blocking slots and why each blocks, then warn once for every shut
gate, naming each slot and its reason and stating the consequence that
no codes are issued for the entry. The gate is re-evaluated on every
refresh, so reporting repeats only when the blocking set changes, and
notes when issuance resumes.

No new persistent notification: the unreadable condition already
raises one, and an unmatched code is usually the transient remains of
a departed booking, which is exactly the notification noise removed in
 #822.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:28:32 -07:00
Andrew GrimbergandCopilot dac23bcfd0 Feat(diagnostics): Add config entry diagnostics
Triaging #830 required asking the reporter to grep raw
home-assistant.log files because nothing exposed the integration's
internal state. The allocator and reconciliation projections already
existed but had no user-facing surface.

Add the standard async_get_config_entry_diagnostics hook so the
Download diagnostics button appears for Rental Control. The payload
bundles the entry configuration, standing slot exclusions, the shared
allocator projection, the latest reconciliation snapshot, per-slot
Keymaster observations and the current reservation set, which is what
a maintainer needs to explain a withheld issuance without a round
trip.

Door codes never reach the payload: slot and reservation rows report
only code presence and code length, and async_redact_data scrubs
code-bearing keys as defence in depth. Guest-identifying text and the
calendar URL are redacted too, since diagnostics downloads are
routinely pasted into public issues.

Closes #832

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:23:23 -07:00
Andrew Grimberg efa4d82a1b Merge pull request #834 from tykeal/fix/adoption-gate-invalid-code
Fix(allocator): Account skipped invalid-code slots
2026-10-22 14:22:21 -07:00
Andrew GrimbergandCopilot 7267e39778 Fix(allocator): Account skipped invalid-code slots
A managed slot whose observed code is not exactly code_length decimal
digits was skipped during adoption without being recorded in
AdoptionRequests.suppressed_slots, unlike the suppression and rotation
skip branches that surround it.

_adoption_complete subtracts suppressed slots from the readable coded
slots it requires to be adopted, so an unaccounted skip leaves the slot
in that set permanently and the gate can never open. Because a closed
gate withholds allocation for the whole entry, a single slot holding a
wrong-length or non-decimal code stopped door code issuance for every
reservation on the config entry, with no path to recovery.

Such a code is fully readable and can never collide with an issued
code, which is always code_length decimal digits, so declining to adopt
it is correct but blocking issuance is not. Accounting the slot as
suppressed makes the branch behave like its siblings while keeping the
diagnostic warning users rely on.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:16:05 -07:00
Andrew Grimberg 8c33b7a60e Merge pull request #837 from tykeal/ci/grype-blanket-ignores
CI(grype): Ignore HA-inherited deps wholesale
2026-10-22 14:15:49 -07:00
Andrew GrimbergandCopilot 969984947b CI(grype): Ignore HA-inherited deps wholesale
The per-CVE, per-version ignore entries in .grype.yaml stopped
matching as soon as Home Assistant bumped its pins, so the Grype
Audit SBOM job failed again on pillow 12.3.0 and uv 0.12.23 while
the config still referenced pillow 12.2.0, PyJWT 2.12.1 and
cryptography 48.0.1.

pillow, uv, PyJWT and cryptography reach the SBOM only as
transitive dependencies of Home Assistant, which is a dev/test
only optional dependency and is never shipped to users. Home
Assistant pins each of them exactly, so we cannot upgrade them
independently and Dependabot security updates for them fail as
unresolvable. Waiting on an upstream bump is the only remedy
available to this repository.

Replace the stale entries with blanket, version-less and
vulnerability-less rules so they keep matching across upstream
bumps instead of requiring a hand rewrite every time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-22 14:02:23 -07:00
Andrew Grimberg 67e7b106d0 Merge pull request #828 from tykeal/dependabot/uv/certifi-2026.10.19
Chore: Bump certifi from 2026.4.22 to 2026.10.19
2026-10-22 09:48:12 -07:00
Andrew Grimberg c538b4bade Merge pull request #825 from tykeal/dependabot/github_actions/lfreleng-actions/python-audit-action-0.6.1
Chore: Bump lfreleng-actions/python-audit-action from 0.6.0 to 0.6.1
2026-10-22 09:48:08 -07:00
Andrew Grimberg 500484b699 Merge pull request #826 from tykeal/dependabot/pre_commit/pre-commit-hooks-116d833335
Chore: Bump https://github.com/astral-sh/ruff-pre-commit from v0.17.0 to 0.17.1 in the pre-commit-hooks group
2026-10-22 09:48:04 -07:00
Andrew Grimberg 621510a3be Merge pull request #829 from tykeal/dependabot/github_actions/lfreleng-actions/draft-release-promote-action-0.1.6
Chore: Bump lfreleng-actions/draft-release-promote-action from 0.1.5 to 0.1.6
2026-10-22 09:48:00 -07:00
Andrew Grimberg 6ebe50b81c Merge pull request #827 from tykeal/dependabot/github_actions/home-assistant/actions/hassfest-cd6f3635a740c84d8a14f6121a90068b8ece6bf6
Chore: Bump home-assistant/actions/hassfest from 50baf3a307f87100cf550d4ae127746595030d5d to cd6f3635a740c84d8a14f6121a90068b8ece6bf6
2026-10-22 09:47:55 -07:00
dependabot[bot] 9ab95f13aa Chore: Bump lfreleng-actions/draft-release-promote-action
Bumps [lfreleng-actions/draft-release-promote-action](https://github.com/lfreleng-actions/draft-release-promote-action) from 0.1.5 to 0.1.6.
- [Release notes](https://github.com/lfreleng-actions/draft-release-promote-action/releases)
- [Commits](https://github.com/lfreleng-actions/draft-release-promote-action/compare/e420b392557c9b1eea642f0d54dec4ff39086d5f...3959dd702aab09b2885a68e57f6608003a6aa2bd)

---
updated-dependencies:
- dependency-name: lfreleng-actions/draft-release-promote-action
  dependency-version: 0.1.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 11:04:19 +00:00
dependabot[bot] 20265701ec Chore: Bump certifi from 2026.4.22 to 2026.10.19
Bumps [certifi](https://github.com/certifi/python-certifi) from 2026.4.22 to 2026.10.19.
- [Commits](https://github.com/certifi/python-certifi/compare/2026.04.22...2026.10.19)

---
updated-dependencies:
- dependency-name: certifi
  dependency-version: 2026.10.19
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-21 00:58:33 +00:00
dependabot[bot] 1f88ee4a60 Chore: Bump home-assistant/actions/hassfest
Bumps [home-assistant/actions/hassfest](https://github.com/home-assistant/actions) from 50baf3a307f87100cf550d4ae127746595030d5d to cd6f3635a740c84d8a14f6121a90068b8ece6bf6.
- [Release notes](https://github.com/home-assistant/actions/releases)
- [Commits](https://github.com/home-assistant/actions/compare/50baf3a307f87100cf550d4ae127746595030d5d...cd6f3635a740c84d8a14f6121a90068b8ece6bf6)

---
updated-dependencies:
- dependency-name: home-assistant/actions/hassfest
  dependency-version: cd6f3635a740c84d8a14f6121a90068b8ece6bf6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 16:02:54 +00:00
dependabot[bot] b6f7d03be7 Chore: Bump https://github.com/astral-sh/ruff-pre-commit
Bumps the pre-commit-hooks group with 1 update: [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit).


Updates `https://github.com/astral-sh/ruff-pre-commit` from v0.17.0 to 0.17.1
- [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases)
- [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/44b5042edab9382aba712eed4f9c7bab7a3d6a3e...38bfb07c0c64c304f47eb52090f8288150c77730)

---
updated-dependencies:
- dependency-name: https://github.com/astral-sh/ruff-pre-commit
  dependency-version: 0.17.1
  dependency-type: direct:production
  dependency-group: pre-commit-hooks
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 16:02:27 +00:00
dependabot[bot] 3dcd93853e Chore: Bump lfreleng-actions/python-audit-action from 0.6.0 to 0.6.1
Bumps [lfreleng-actions/python-audit-action](https://github.com/lfreleng-actions/python-audit-action) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/lfreleng-actions/python-audit-action/releases)
- [Commits](https://github.com/lfreleng-actions/python-audit-action/compare/3cd736d87b0bd0f4649f0f9ba6a84de8f7953a12...47af793a2a28882b10ceb9109646cbbcecc2e7b0)

---
updated-dependencies:
- dependency-name: lfreleng-actions/python-audit-action
  dependency-version: 0.6.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-19 16:02:08 +00:00
Andrew Grimberg e393cbd686 Build(deps): Add Dependabot pre-commit ecosystem
Build(deps): Add Dependabot pre-commit ecosystem
2026-10-19 09:01:22 -07:00
Andrew GrimbergandClaude 21db5cd3e5 Chore(pre-commit): Slow ci autoupdate to quarterly
Dependabot now owns hook rev updates on a weekly cadence. Leaving
pre-commit.ci's autoupdate on its default schedule would have both
systems rewriting the same rev values, producing duplicate and
conflicting pull requests.

There is no way to disable the autoupdate entirely, so set the
slowest supported schedule. With Dependabot running weekly, the
quarterly check will usually find nothing stale and stay silent.
The existing autofix and autoupdate commit messages, autofix_prs,
and the skip list are deliberately left untouched.

Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-19 08:14:03 -07:00
Andrew GrimbergandClaude 32bbab4ed4 Build(deps): Add Dependabot pre-commit ecosystem
Dependabot natively supports the pre-commit ecosystem, parsing
.pre-commit-config.yaml and updating hook rev values along with the
inline '# frozen:' version comments that every remote hook in this
repository uses. Its commits carry a Chore prefix matching our
Conventional Commit convention and are GitHub-signed, consistent
with the signed-commit practice used throughout the repository.

Hook bumps are grouped into a single pull request rather than one
pull request per hook. The local hook repository has no upstream
rev and is skipped automatically by Dependabot.

Co-authored-by: Claude <claude@anthropic.com>
Signed-off-by: Andrew Grimberg <tykeal@bardicgrove.org>
2026-10-19 08:13:49 -07:00
Andrew Grimberg 29892b90e0 CI(release): Pin Python for the SBOM job
CI(release): Pin Python for the SBOM job
2026-10-18 16:47:04 -07:00