Skip to content

feat: record each output-device drive and its result (TRA-1314) - #780

Draft
mikestankavich wants to merge 4 commits into
mainfrom
feat/tra-1314-record-each-output-device-drive-and-its
Draft

mikestankavich wants to merge 4 commits into
mainfrom
feat/tra-1314-record-each-output-device-drive-and-its

Conversation

@mikestankavich

Copy link
Copy Markdown
Contributor

Draft: held out of 1.6 by the planning session. One question on the ticket is still open (below).

Adds output_drives, one row for each output-device drive. Each row records which device was driven, on or off, why, and whether it worked. Until now alarm_events only said that a rule fired for a read. A drive to an offline reader left a row that looked like a working alarm, one row covered every device at the location, and presence age-out OFFs left nothing at all.

What changed

  • New table. Migration 000055 adds output_drives: device, alarm id (set on the ON a fire sends, otherwise null), edge, cause, result (ok / sent / error) with error text, latency and drive time. It has RLS like alarm_events.
    • The cause is one of alarm, refresh, age_out, test, reset or delete.
  • Geofence engine. It now writes the alarm row before driving, so each ON can name its fire, then records every drive:
    • fire ONs
    • presence refreshes
    • the presence age-out OFF, which had no record before
  • Undelivered alarms. An alarm whose every drive errored is logged as a warning and counted in geofence_alarms_undelivered_total.
  • Manual actions. The output-device test, reset and delete actions record their drives too, failures included. Commissioning no longer needs SSH to show that a horn was driven.
  • Best-effort writes. A failed alarm-row or drive-row write never stops a drive. Drive rows are written even if the HTTP client hangs up.
  • New read function. ListOutputDrives returns one device's drives, newest first. That is the per-output query TRA-945 needs.

Decisions to review

  • alarm_events stays the detection record; the drive rows are the delivery record. The ticket offered "write alarm_events only on success". TRA-1303 decided on 2026-11-05 to write the alarm and its notification trigger in the scan transaction, before any drive, so that option is closed. geofence_alarms_fired_total's help text now says detected.
  • sent instead of ok for MQTT transports. A CS463 GPO drive goes through readercontrol.Client.Notify, which is fire-and-forget, and a Shelly over MQTT is a bare publish. A powered-off reader still lets the publish succeed. So those drives record sent, and only an HTTP device's drive can be ok.
    • This is why the PR stays a draft. Recording whether the reader actually acted is still open. The options are written up on the ticket; async reply correlation is the recommendation.
  • Migration number 000055. It is also claimed by feat: list sent notification alerts (TRA-1298) #748, feat: allow location changes that record a gate crossing #754 and feat: index notification contact lookups #768. I chose a duplicate over 000056 on purpose. golang-migrate never applies a lower version than the current one, so if 56 merged first, a later 55 would be skipped silently. A duplicate fails loudly. Whoever merges second renumbers.
  • Conflicts with TRA-1303. refactor: separate geofence decisions from driving outputs #758 and feat: record geofence alarms inside the scan transaction #761 also rewrite engine.go, presence.go and alarm_events.go. Both of them and this PR write the alarm before driving, so the two designs agree. Whichever lands second rebases.
  • Horn latency. The alarm insert now happens before the first ON. Each drive row is written right after its drive, so device N+1 waits for device N's row (a few ms).

Coverage against the ticket

  • Every ON and OFF drive leaves one row naming the device and the result: done.
  • Offline reader shows result=error: done for HTTP failures and broker-publish failures. Not done for a CS463 GPO on a live broker, which records sent. That is the open question.
  • TRA-945 can scope its activity log per output: done (ListOutputDrives).

Known minor gaps (not fixed here)

  • Error text is not stripped of NUL bytes or invalid UTF-8, so such a row would be rejected and lost.
  • output_drives has no retention policy. A continuously held presence output writes about 8,640 refresh rows a day.
  • Handler drives (test, reset, delete) don't increment geofence_output_drives_total.
  • driven_at mixes the read's receive time with wall-clock time for age-out.
  • With several devices, WithoutCancel lets one message's processing run past its 30 s deadline.

Testing

  • just validate passes.
  • Integration tests pass for storage, the outputdevices handler, mustering, serve and orgs, against a throwaway TimescaleDB.
  • go test -race ./internal/geofence passes.
  • Not tested on hardware. Worth checking on a CS463:
    • A test fire on a live reader records test/sent for both the ON and the OFF.
    • A powered-off reader records sent, not error (expected, pending the question).

Refs TRA-1314

🤖 Generated with Claude Code

Mike Stankavich and others added 4 commits November 8, 2026 22:06
alarm_events says a rule fired for a read. It does not say which devices
were driven or whether the drive reached them, so a drive to an offline
reader looks the same as one that sounded the horn. output_drives is the
delivery record: device, edge, cause, ok/error with the error text, and
latency. InsertAlarmEvent now returns the row id so the drives that
deliver an alarm can name it.

Refs: TRA-1314

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Evaluate now writes the alarm_events row before driving, so each ON
names the fire it delivered, then records one output_drives row per
drive: the fire's ONs (cause alarm), a held presence output's refreshes
(refresh) and the age-out OFF (age_out), which had no record at all.

A fire whose every drive failed is logged as a warning and counted in
geofence_alarms_undelivered_total, so a drive to an offline reader no
longer reads as a working alarm. A failed alarm or drive-row write
never stops or delays a drive.

Refs: TRA-1314

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The manual actions drive the same devices a geofence fire does, and
commissioning proves a horn works with the test action. Each now leaves
an output_drives row (cause test, reset or delete), failures included,
so that proof no longer needs SSH to the reader's own log.

Refs: TRA-1314

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CS463 GPO drive goes through readercontrol's fire-and-forget Notify,
and a Shelly over MQTT is a bare publish: success means the broker took
the message, not that the device acted. A powered-off reader therefore
recorded result=ok. An mqtt-transport drive that publishes cleanly is
now result=sent; only an http device's drive can be ok.

geofence_alarms_fired_total's help now says detected, not sounded.

Refs: TRA-1314

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant