<feed xmlns='http://www.w3.org/2005/Atom'>
<title>luke/mattertimesync/.prettierignore, branch main</title>
<subtitle>Unnamed repository; edit this file 'description' to name the repository.
</subtitle>
<id>https://src.nth.io/luke/mattertimesync/atom?h=main</id>
<link rel='self' href='https://src.nth.io/luke/mattertimesync/atom?h=main'/>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/mattertimesync/'/>
<updated>2026-07-27T02:13:51+00:00</updated>
<entry>
<title>Implement mattertimesync: one-shot Matter time synchronization CLI</title>
<updated>2026-07-27T02:13:51+00:00</updated>
<author>
<name>Luke Hoersten</name>
<email>luke@hoersten.org</email>
</author>
<published>2026-07-26T10:38:33+00:00</published>
<link rel='alternate' type='text/html' href='https://src.nth.io/luke/mattertimesync/commit/?id=0e536daebe4cceb27eaccdbc4fc8ca066dff71b9'/>
<id>urn:sha1:0e536daebe4cceb27eaccdbc4fc8ca066dff71b9</id>
<content type='text'>
A standalone CLI Matter controller that joins Matter devices' existing
setups as a secondary administrator (multi-admin) and sets their clocks
via the standard Time Synchronization cluster. Built on matter.js 0.17.6
using the CommissioningController API. One-shot runs from a systemd
timer, no daemon: being a secondary controller is fabric membership, not
a running process, so each invocation loads the persisted keys,
discovers the device via operational DNS-SD, opens a fresh CASE session,
does its work, and exits.

- TypeScript scaffold: strict tsc, oxlint, prettier, vitest (66 tests:
  Matter epoch conversion, config validation, IANA timezone offsets and
  exact DST transition search, atomic state writes, pairing-code
  parsing, clock-delta reporting, capability planning against the
  captured device fixture)
- Validated JSON configuration with bigint-safe values; unknown fields
  rejected loudly
- Persistent controller fabric with the same identity across restarts.
  The fabric doubles as the device registry: every node commissioned
  onto it is kept in sync, and membership changes only through
  commission and decommission, so no separate device list can drift
- On-network multi-admin commissioning from a manual or QR pairing code;
  any number of devices, one per-device pairing code each. A device
  already on the fabric rejects re-commissioning via fabric conflict
- sync, validated against real hardware (IKEA ALPSTUGA air quality
  monitor over Thread, commissioned alongside Apple Home): refuses to
  run unless the host is NTP-synchronized, then per node reads utcTime,
  reports the correction with direction ("device clock was 1m 23s
  behind", handling unset clocks in exact bigint microseconds), writes
  SetUTCTime, SetTimeZone (standard offset plus IANA name), and
  SetDSTOffset (transition list bounded by the device's capacity), and
  verifies by reading the clock back. Capability-driven throughout:
  UTC-only devices degrade gracefully, cluster-less devices are skipped
  with a warning. Devices whose firmware encodes Unix-epoch time on the
  wire are detected by the exact 946684800s shift and reported
  corrected. Note matter.js's TlvEpochUs API convention: values are
  Unix-epoch microseconds at the API boundary; the library converts to
  Matter epoch on the wire
- Commands: nodes, inspect (human/JSON; also reads the fabric table
  fabric-unfiltered, matching our own entry by fabric ID plus root
  public key and flagging likely-stale orphans; strictly read-only,
  since this tool never uses its admin rights against other fabrics'
  entries), status, and decommission (each device drops our fabric
  while staying paired to its primary ecosystem; targets all devices
  unless --node narrows it)
- Logs (service and matter.js library, plain format) go to stderr;
  stdout carries only command output, so --json (available on nodes,
  inspect, sync, status, fabrics) is always clean parseable JSON with
  64-bit values as decimal strings. Node's node:sqlite
  ExperimentalWarning is filtered before matter.js loads
- systemd oneshot service (dedicated non-root user, hardening) plus
  hourly timer with randomized delay
- Deployment as a single esbuild bundle (mattertimesync.mjs, ~4.5 MB):
  all runtime deps are pure JavaScript, so the target needs only
  Node 20+, with no npm, registry access, or build toolchain. The Bun
  sqlite driver stays external behind its runtime guard and node:sqlite
  is aliased to a lazy shim so the bundle runs under plain Node. npm
  pack remains an alternative install path
</content>
</entry>
</feed>
