src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-07-28 21:31:24 -0500
committerLuke Hoersten <[email protected]>2026-07-28 21:31:24 -0500
commit5c81b970819e8d38413a5dc02d56b7f182715e8a (patch)
tree32712fdffe4eaa86911199915c710ba876fcdb2d
parenta35610f29ec870695339015ef5bd4d3ab2ade3c2 (diff)
README: add CLI command/option tables near the top; tighten design principlesHEADmain
-rw-r--r--README.md99
1 files changed, 36 insertions, 63 deletions
diff --git a/README.md b/README.md
index d30698e..a20766a 100644
--- a/README.md
+++ b/README.md
@@ -16,6 +16,28 @@ ecosystem (e.g. Apple Home) keeps working unchanged.
There is no daemon: each invocation builds the Matter stack, connects, syncs, and exits.
+## Commands
+
+| Command | Kind | Description |
+| --- | --- | --- |
+| `status [-n <id>] [-j]` | local, read-only | Controller config, identity, and per-device sync state. No device contact. |
+| `inspect [-n <id>] [-j]` | fabric read | Connect to each device (or one with `-n`) and dump identity, time-sync capabilities, and our fabric entry. |
+| `commission <code> [-j]` | fabric write | Join a device as an additional Matter admin. The pairing code is used once, never logged or stored. |
+| `sync [-n <id>] [-t <hh:mm>] [-j]` | fabric write | Set each device's clock: UTC time, time zone, DST offsets. |
+| `decommission [-n <id>] [-j]` | fabric write | Drop this controller's fabric from each device (or one with `-n`). Primary ecosystems are untouched. |
+
+| Option | Description |
+| --- | --- |
+| `-c, --config <path>` | Config file. Default `/etc/mattertimectl/config.json`. |
+| `-n, --node <id>` | Target one device. `sync` and `decommission` default to all commissioned devices. |
+| `-t, --time <hh:mm>` | `sync` only: set this wall-clock time today (e.g. `16:35`, `4:35pm`) instead of now. The operator becomes the time source and the NTP gate is skipped. |
+| `-j, --json` | Machine-readable JSON on stdout (64-bit values as decimal strings). Logs stay on stderr, so `--json` is always clean. |
+| `-h, --help` / `-V, --version` | Print help / version. |
+
+No command writes local state alone; the registry and sync results change only as part of a
+fabric-writing command. Exit codes: `0` success, `1` runtime failure (the timer retries), `2` usage or
+config error.
+
## How it works
- The device stays commissioned to its primary ecosystem and attached to its existing Thread or Wi-Fi
@@ -31,69 +53,20 @@ There is no daemon: each invocation builds the Matter stack, connects, syncs, an
## Design
-Guiding principles:
-
-- **One-shot CLI, not a daemon.** Being a secondary Matter controller is fabric membership, not a
- running process. Each invocation loads the persisted fabric identity, races the Matter transport and
- mDNS pumps against the one operation, and exits. Clock staleness is bounded by the systemd timer
- interval.
-- **Capability-driven, never vendor-specific.** Any device exposing the standard Time Synchronization
- cluster (0x38) works. SetTimeZone/SetDSTOffset are gated on the device's feature map and bounded by
- its DSTOffsetListMaxSize; devices without the cluster are skipped with a warning; devices whose
- firmware encodes Unix-epoch time on the wire are detected by the exact 946,684,800s shift and
- reported corrected.
-- **The fabric is created exactly once.** First run mints the controller's root CA and operational
- identity (RCAC-direct mode); every later run loads it. The identity lifecycle is guarded: runs hold
- an exclusive lock on the storage directory, a fresh identity is only ever minted over provably
- virgin storage (no fabric, no identity file, no registry entries), the one partial state an
- interrupted first run can produce is discarded automatically, and any state a commissioned device
- could depend on is refused loudly rather than overwritten. Reconnection uses Matter operational
- discovery (mDNS), never a stored IP address.
-- **Fail safely.** Never recommissions automatically; never pushes time unless the host clock is
- trusted, checked by directly measuring its offset against an NTP server (one SNTP query, accepted
- under 1s; `timedatectl` as fallback when no server is reachable). Every sync is verified by reading
- the clock back and comparing against the written instant; non-zero exit lets the timer retry. CASE
- connects retry three times with backoff, since an mDNS resolve answer can be lost per-attempt.
-- **Flat files, no database.** All state is plain files under `storagePath`: rs-matter's KV blobs
- (`matter/k_XXXX`, written atomically via temp+rename), `identity.json` (the root CA private key;
- mode 0600, written once and never touched by a sync), and `devices.json` (one record per
- commissioned device: cached names plus latest sync results).
-- **Time is jiff end to end.** Timestamps are `jiff::Timestamp`; Matter-epoch microseconds exist only
- at the wire boundary. DST transitions come directly from the IANA tzdb transition table, never from
- offset probing. Time zones are IANA names, never fixed offsets.
-- **Intentionally small.** No GUI, database, cloud, QR-code pairing (the primary ecosystem always
- shows the numeric code), general Matter administration, or debugging surfaces beyond what operating
- this tool needs.
-
-## Commands
-
-Local, read-only (no device contact):
-
-```bash
-mattertimectl status [--node <id>] [--json] # config, identity, and per-device sync state
-```
-
-Fabric interrogation (connects to the device, reads only):
-
-```bash
-mattertimectl inspect [--node <id>] [--json]
-```
-
-Fabric-writing:
-
-```bash
-mattertimectl commission <pairing-code> # writes the device's fabric table + local storage
-mattertimectl sync [--node <id>] [--time <hh:mm>] [--json] # set clock, time zone, DST offsets
-mattertimectl decommission [--node <id>] # device(s) drop this controller's fabric
-```
-
-No command writes local state alone; the registry and sync results change only as part of a
-fabric-writing command.
-
-`--node <id>` targets one device; `sync` and `decommission` target all commissioned devices by
-default. `sync --time 16:35` (or `4:35pm`) sets that wall-clock time, today in the configured zone,
-instead of the current time; the operator is then the time source and the NTP gate is not enforced. Logs go to stderr; stdout carries only command output, so `--json` is always clean JSON.
-Exit codes: 0 success, 1 runtime failure (timer retries), 2 usage/config error.
+- **One-shot, not a daemon.** Being a secondary controller is fabric membership, not a process. Each
+ run loads the identity, does one operation, and exits; staleness is bounded by the timer interval.
+- **Capability-driven.** Any device with the standard Time Synchronization cluster (0x38) works.
+ Time-zone/DST writes are gated on the device's feature map; devices without the cluster are skipped.
+- **One fabric, ever.** The controller identity is minted once, only over provably empty storage, under
+ an exclusive lock. Partial first-run state is discarded; state a device depends on is never overwritten.
+- **Fail safe.** Never recommissions on its own. Won't push time unless the host clock is NTP-trusted,
+ and verifies every write by reading the clock back; a non-zero exit lets the timer retry.
+- **Flat files, no database.** All state lives under `storagePath`: rs-matter KV blobs, `identity.json`
+ (the root CA key, mode 0600), and `devices.json` (per-device names and last sync).
+- **Time via jiff.** Timestamps are `jiff::Timestamp`; Matter-epoch microseconds live only at the wire.
+ DST comes from the IANA tzdb, never offset probing; zones are IANA names, never fixed offsets.
+- **Small on purpose.** No GUI, cloud, QR pairing, general Matter administration, or debug surfaces
+ beyond what operating this tool needs.
## Configuration