Rearchitect so a single poller process is the sole owner of all SAS/SMART/
SES hardware I/O, serialized behind one gate and paced, writing results to
Redis. The API/web and MQTT publisher become pure Redis readers — they no
longer issue any subprocess and can restart freely without touching the bus.
This addresses backplane/expander stress from concurrent + restart-triggered
SMART/SES storms (the prior model re-ran a hardware sweep on every container
start, and polled sg_ses 0x02+0x07 every 60s; 0x07 errored on the IOM6/
Xyratex expanders).
- poller.py: paced sweep (inventory, SMART per-drive w/ gap, SES 0x02, ZFS,
host/MegaRAID), startup jitter, single-instance Redis lock, LED-queue worker
- services/hwgate.py: global serialization semaphore (POLL_CONCURRENCY=1)
- services/store.py: Redis as the only producer<->consumer interface + LED queue
- services/health.py: shared drive-health classifier (fixes overview double-count)
- gate smartctl/sg_ses/zpool/ledctl; drop sg_ses 0x07 from the hot path
- routers + temps read-only from the store; main.py drops the in-process poller
- compose: 3 services (privileged poller + unprivileged app + redis) w/ pacing knobs
Some enclosures (Xyratex/NetApp shelves) leave SES element descriptor
strings blank; sg_ses prints the literal '<empty>'. Skip it so temp
sensors fall back to a generic 'Temp N' label instead of '<empty>'.
- enclosure: fetch SES element descriptor page (0x07) and label temp/fan/
psu/voltage elements with human-readable names
- temps service + /api/temps: per-enclosure hotspot (max across SES sensors
and housed drive temps) plus named SES sensor list
- mqtt_publisher: paho-mqtt with HA discovery (device per enclosure, hotspot
+ per-sensor entities), LWT availability, opt-in via MQTT_HOST
- secrets: OpenBao KV v2 reader; MQTT creds sourced from secret/home_assistant
with env fallback
- compose/requirements/README updated
Xyratex HB-1235 enclosures have non-functional temperature sensor
slots that report Unrecoverable/Unknown at 0°C. The SES header
rolls these into UNRECOV=1, causing a false CRITICAL flag. Now we
skip dead sensors (0°C + non-OK status) and derive overall_status
from the actual parsed elements instead of the raw header counts.
Dual-IOM shelves (e.g. NetApp DS2246 with two IOM6 modules) create
two sysfs enclosure entries. The secondary path reports populated
slots via SES but the kernel only links block devices through the
primary path. Filter these out during discovery to avoid showing
ghost enclosures with no drive data.
Parse sg_ses --page=0x02 output to surface enclosure-level health data
including power supply status, fan RPMs, temperature sensors, and voltage
rails. Failed/critical components are reflected in the overview totals
and shown as status pills in the enclosure card header with an expandable
detail panel.
- Vite + React frontend with dark-themed dashboard, slot grid per
enclosure, and SMART detail overlay
- ZFS pool membership via zpool status -P
- Multi-stage Dockerfile (Node build + Python runtime)
- Updated docker-compose with network_mode host and healthcheck