Every number the collector samples becomes a sensor with a stable dotted ID (cpu.core.3.usage, fan.nct6775.fan2, fs.root.used_pct). Clients discover them from the catalog and ask for exactly what they want, including as a shaped query: send the object you want, get it back with the values filled in. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
3.7 KiB
Sensors — the standard read interface
Everything Ham Lab measures is a sensor: a stable dotted ID, a little metadata, a current value. Any consumer — Ham Lab's own dashboard, Glance, Home Assistant, a shell script — discovers them from the catalog and asks for exactly the ones it wants.
Code: internal/sensors (types, registry, selectors, shaped query) and
internal/metrics/sensors.go (the collector as a provider). The registry measures
nothing; providers do. A new source of sensors implements sensors.Provider and is
added to the registry in api.New.
IDs
cpu.usage cpu.core.<n>.usage cpu.temp load.1|5|15 uptime
mem.total|used|avail|free|cached|buffers|used_pct swap.total|used|avail|used_pct
temp.<chip>.<label> fan.<chip>.<label>
net.<iface>.rx|tx|rx_total|tx_total|up
disk.<dev>.read|write|util|size|temp
fs.<mount>.size|used|used_pct
ups.battery|runtime|load|load_pct|in_v|out_v|on_battery|state|source
- Segments are lower case
[a-z0-9_-]:Package id 0→package_id_0,/mnt/tank→mnt_tank,/→root. The real names are in the sensor'slabels. - Built from names the hardware reports, never enumeration order (
hwmon3moves between boots). Two identical chips collide; the second gets_2. - Which sensors exist differs per machine. That is the point of the catalog.
A sensor
{ "id": "temp.coretemp.package_id_0", "name": "coretemp Package id 0",
"kind": "temperature", "unit": "°C", "value": 51, "t": 1789826600878,
"warn": 105, "crit": 105, "labels": { "chip": "coretemp", "label": "Package id 0" } }
value is a number, bool or string. t is when it was really read, in unix ms — slow
sections (filesystems 30 s, UPS 10 s) are older than the tick. min/max are the scale
(0–100 for percentages, 0–total for used bytes); warn/crit are the hardware's own
thresholds where it states them.
Endpoints
All behind the same auth as the rest of /api (session cookie or Authorization: Bearer).
On a hub, every one is also at /api/nodes/<machine>/sensors… for each managed machine.
GET /api/sensors |
Catalog, with current values. ?ids=fan.*,cpu.usage narrows it. |
GET /api/sensors/{id} |
One sensor. ?format=text → the bare value as text/plain. |
GET /api/sensors/{id}/history |
{id, unit, t[], v[]} — what the ring holds (300 samples at the sampling interval). Numbers only. |
POST /api/sensors/query |
Shaped query, below. |
GET /api/sensors/query?k=selector&… |
The same, flat, for consumers that can only fetch a URL. |
A selector is an exact ID or a glob. * crosses dots: fan.* is every fan,
cpu.core.*.usage every core.
Shaped query
Send the object you want, with selectors where the values go. Get it back filled in.
POST /api/sensors/query
{ "cpu": "cpu.usage", "fans": "fan.*", "box": { "temp": "cpu.temp", "up": "uptime" } }
→ { "cpu": 12.4,
"fans": { "fan.nct6775.fan1": 820, "fan.nct6775.fan2": 1140 },
"box": { "temp": 41, "up": 80520 } }
- Exact ID → its value;
nullif this machine has no such sensor (so one query works across machines that differ). - Glob → an object keyed by ID;
{}if nothing matches. - Objects and arrays are walked; anything else in a query is a 400.
?full=1→ every value is the whole sensor object instead of the bare value. (In the GET formfullis therefore not usable as a key.)
Not yet
Read-only scoped tokens, Prometheus /metrics, a push stream, widgets built on this,
an embeddable widget page, and non-metric sensors (services, SMART, ZFS, updates). Until
scoped tokens exist, do not hand a Ham Lab token to a dashboard — today's tokens are
full control of the machine.