Skip to content

feat: a credential use shows in the logbook of the lock it was used on, and the user card lists recent uses - #1796

Merged
raman325 merged 5 commits into
mainfrom
feat/credential-use-logbook
Oct 30, 2026
Merged

raman325 merged 5 commits into
mainfrom
feat/credential-use-logbook

Conversation

@raman325

@raman325 raman325 commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Proposed change

A forum user asked how to see who unlocked which door, and when. The data was already recorded: the user's credential_used event entity holds target, credential_type and operation for every use. But Home Assistant didn't show it anywhere they'd look. The lock's activity said "No cause was recorded", and clicking Last used on the user card opened a more-info dialog that showed only the event type ("pin").

1. Each use goes into the logbook of what it was used against. async_fire_credential_used (the one function every use goes through, whether a lock reported it or use_credential was called) now also fires a manual logbook entry (EVENT_LOGBOOK_ENTRY, with domain=lock_code_manager and entity_id=target). The lock's activity, the lock's device page and the Logbook filtered to that lock all show entries like "Unlocked by Alice with a PIN", or "Alice used a PIN" when the device didn't say what it did. The person goes in the message because the frontend never shows a logbook entry's own name field when the entry has an entity_id. The Logbook panel shows the entity's name in that place (logbook-entry-model.ts, display?.primary ?? entry.name), and the entity's own activity view shows the message alone (name-detail="none").

Why a manual entry instead of a logbook.py that describes our own event:

  • When the logbook builds an entity's view, it only includes custom events from the integrations that own that entity (async_determine_event_types). For a lock, that's Z-Wave JS or Matter, never us.
  • The query also matches on an entity_id or device_id key in the event data, and our payload has target.
  • So a described event would only reach the unfiltered Logbook. A manual logbook_entry is the one event type the logbook includes for any entity named in its entity_id.

Precedent, for the record:

  • No core integration writes logbook_entry.
  • Of the 92 most-installed custom integrations I could check, one does: Better Thermostat, and only on its own entity.
  • Integrations that act on another integration's entity (Alexa, HomeKit) get credit by calling the service with a shared context ID. That doesn't work here, because LCM doesn't make the lock's state change; it only hears about it afterwards.
  • So this uses a public API somewhere no integration has needed to before. If lock integrations ever gain a way to record who unlocked, that should replace this.

This doesn't fill in "No cause was recorded" on the lock's own state row. The frontend builds the cause from shared context IDs, and the lock integration sets that context. The use shows up as its own row next to the state change.

Context. The use_credential action passes the service call's context to both the bus event and the logbook entry. A keypad automation that reports a use shows up in the logbook as that entry's cause, and automations reacting to the bus event trace back to it. Uses a lock observed pass None: the lock's state change already has the lock integration's context, and sharing it would tell the logbook the unlock caused the use. context is a required argument, so every caller has to choose.

2. The user card lists recent uses. Clicking Last used now expands the user's recent uses, up to 10. Each row shows what the credential was used against, the kind of credential, what the device did, and when. Full history underneath opens the more-info dialog, which is what clicking the row used to do.

  • While the list is open, it subscribes to history/stream for the event entity (last 30 days, attributes included). This is what Home Assistant's own more-info history uses. It sends what the recorder holds, then the states it hadn't committed yet, then each new state as it happens. So no use is missed, whether it was still waiting on a recorder commit, happened while the card was off screen, or came across a reconnect. Nothing new is stored.
  • The stream closes when the list is collapsed, when the card leaves the page, when the card switches user, and when the slot comes to name someone else (the list closes too). Re-expanding opens a fresh stream. Messages from a closed stream are dropped. The card keeps only as many rows as it shows. The stream asks only for state changes, since every use moves the state.
  • Rows recorded before 6.0, which have no target, are skipped.
  • The source entity is never read, since its state can be the code that was typed.
  • Each kind is labelled with Home Assistant's own event_type translation (hass.formatEntityAttributeValue), so the list names a kind the same way the entity's history does.
  • The row has no aria-label, so its visible text (the lock and the time) is what screen readers announce.

Messages are English, not translated. LCM ships only en.json, and the cards are hard-coded English, so translating at write time would serve no one.

Why the entry is fired directly. EVENT_LOGBOOK_ENTRY is fired directly rather than through logbook.async_log_entry, which fires exactly the same event. Importing the logbook would pull its recorder and frontend modules into every provider. Depending on it would hold LCM's setup behind the recorder's, which can be slow during a database migration. The recorder stores the event whether or not the logbook is loaded yet.

Review findings not addressed here

Four /code-review rounds. Round 3 replaced round 2's hand-kept cache with history/stream. Round 4 found nothing high-severity; its fixes tidied the stream's lifecycle.

I ran /code-review and fixed the commit race, the flicker on reload, rows from before 6.0, the label mismatch and the enum comparison. Left as they are:

  • History of a reused entity ID shows up under the new user. If a removed user's name is reused, the new user's list includes the old user's uses. Home Assistant's own history does the same when an entity ID is reused.
  • The logbook also records uses the event entity drops because their kind isn't in its event types. This matches the bus event, which fires for them too.
  • A caller's context can surface the keypad's state. A caller that reuses the keypad sensor's own context would have the logbook show that sensor's state (possibly the typed code) as the entry's cause. Through an automation the cause is the automation run, not the sensor. That sensor's state is already in the logbook as its own row, so this exposes nothing new.
  • A target with no state shows its raw entity ID in the card's list, where the "Last used" header shows "Used". The entity ID is more useful than nothing.

Testing

  • tests/test_logbook.py: end-to-end with the real recorder and logbook, querying logbook/get_events by the lock's entity ID, the same way its activity view does. Covers a use the lock reported, a use reported through use_credential against a cover, and that nothing is filed under the source. Also checks every pairing of kind and operation, and that no two kinds read the same. Mutation check: filing under source instead of target fails.
  • ts/credential-uses.test.ts: the history parser.
  • ts/slot-card.dom.test.ts: clicks the rendered row and reads the list, including a use that arrives while the list is open and a failed history read. Mutation checks: removing the expand, or removing the step that keeps the current state, fails.
  • tests/test_frontend_contract.py: the card's labels match strings.json.
  • Full Python suite run serially: everything passes. Under -n auto, three real-time pacing tests fail from load and pass on their own; they don't involve credential uses.
  • yarn build prints an existing type error in ts/generate-view.ts (pinActiveEntity), a file this PR doesn't touch.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New feature (which adds functionality)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

  • This PR fixes or closes issue: none filed; it came from a forum question
  • This PR is related to issue:

🤖 Generated with Claude Code

…n, and the user card lists recent uses

Every use is written as a manual logbook entry under its target ("Alice used
a PIN to unlock"), so it appears in the lock's own activity next to the state
change it explains. Clicking "Last used" on the user card now expands the
user's recent uses, read from the recorder's history of their credential-used
event entity, with a link through to the full history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 1d5a779241a6
Copilot AI balanced review requested due to automatic review settings October 29, 2026 03:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@github-actions github-actions Bot added python Pull requests that update Python code javascript Pull requests that update javascript code documentation Documentation changes enhancement New feature or request labels Oct 29, 2026
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.05882% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.87%. Comparing base (7021133) to head (8bf9971).
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
ts/slot-card.ts 95.00% 3 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1796      +/-   ##
==========================================
- Coverage   98.89%   98.87%   -0.02%     
==========================================
  Files          72       73       +1     
  Lines       10299    10396      +97     
  Branches      569      599      +30     
==========================================
+ Hits        10185    10279      +94     
- Misses        114      117       +3     
Flag Coverage Δ
python 99.38% <100.00%> (+<0.01%) ⬆️
typescript 96.39% <96.66%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
custom_components/lock_code_manager/__init__.py 100.00% <ø> (ø)
custom_components/lock_code_manager/ha/services.py 98.96% <100.00%> (ø)
...ustom_components/lock_code_manager/model/events.py 100.00% <100.00%> (ø)
...om_components/lock_code_manager/providers/_base.py 98.88% <ø> (ø)
ts/credential-uses.ts 100.00% <100.00%> (ø)
ts/slot-card.styles.ts 100.00% <ø> (ø)
ts/slot-card.ts 98.00% <95.00%> (-0.32%) ⬇️
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

raman325 and others added 4 commits October 29, 2026 00:22
…ist from review

The frontend never shows a logbook entry's own name when the entry has an
entity_id: the Logbook panel puts the entity's name there and the entity's
activity view shows the message alone. So the person goes in the message:
"Unlocked by Alice with a PIN", or "Alice used a PIN" when the device did not
say what it did.

The card labels kinds with Home Assistant's own event_type translations
(formatEntityAttributeValue) instead of a copied table, reuses rows already
read when re-expanding, drops the list when the card switches to another
user, and lets the row's own text be its accessible name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 3b1b9cd0dc4d
…use in the logbook

async_fire_credential_used now takes the context of whatever reported the use
and fires both the bus event and the logbook entry with it. The use_credential
action passes the service call's context, so the logbook links the entry to
the automation run (or person) that reported it. A use a lock observed passes
None: the lock's state change has the lock integration's context, and sharing
it would tell the logbook the unlock caused the use. The parameter is required
so every caller decides.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: f07172769779
…ntry directly

The card's list now subscribes to history/stream while it is open instead of
reading the history once and splicing live states in by hand. The stream
sends what the recorder holds, then what it had not yet committed, then each
new state, so uses that arrive before a commit, while the card was off screen,
or across a reconnect are no longer missed. The hand-kept cache, the request
counter and the current-state splice are gone. Kept rows are trimmed to what
the list shows. The stream closes on collapse, when the card leaves the page,
when the card switches user, and when the slot comes to name someone else.

The logbook entry is fired as EVENT_LOGBOOK_ENTRY directly instead of through
logbook.async_log_entry, which fires exactly that. That drops the import of
the logbook (and its recorder and frontend modules) from every provider, and
drops the after-dependency that held this integration's setup behind the
recorder's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 5243fc6ceaee
- A closed stream's late messages are dropped by stream identity, not by
  entity ID, so collapsing and re-expanding on the same user cannot mix them in.
- When the slot comes to name someone else, the open list closes instead of
  reopening a stream the row may never show; a stream that cannot open shows
  the error message rather than "Loading…" forever.
- The stream asks for state changes only: every use moves the state, and
  the entity's attribute-only writes are left out.
- mergeUseRows owns which rows are uses; toCredentialUses only names them.
- A credential kind with no phrase yet reads as its value in the logbook
  message instead of raising inside the provider callback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: acf01879bd18
@raman325 raman325 added the lcm-minor Minor version bump label Oct 30, 2026
@raman325
raman325 merged commit 7b600e7 into main Oct 30, 2026
30 of 34 checks passed
@raman325
raman325 deleted the feat/credential-use-logbook branch October 30, 2026 02:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Documentation changes enhancement New feature or request javascript Pull requests that update javascript code lcm-minor Minor version bump python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants