rewrite as a daemon with shutdown on low battery
the oneshot+timer produced 8367 journal lines a day of systemd start/stop noise. now Type=notify with an internal ticker, logging transitions only. hardware sits behind sensor.Sensor; internal/monitor is pure, so the shutdown state machine is tested without a pi. four guards, each tested, because a false poweroff of a box with no physical access is worse than a missed one: - a failed read resets the arming window rather than pausing it - voltage must actually fall across the window, else it is a stuck sensor. this is the only defence against the ac line reading 0 while mains is connected, so it is load-bearing - a settle period stops a restart loop acting on early readings - flapping ac cannot accumulate verified on hardware 2026-08-09: after poweroff on battery the x1208 starts the pi again when mains returns, with no button press. that was the risk that could have made this feature unsafe. a failed tick rewrites the previous sample with sensor_healthy 0 and the failure counter, so last_update still freezes for the staleness alert while the failure stays visible. module path moved to git.mial.net. debian units and the compose overlay deleted; deployment lives in the infra repo.
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
package sensor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.mial.net/mokhtar/x1208-exporter/internal/max17040"
|
||||
"git.mial.net/mokhtar/x1208-exporter/internal/pld"
|
||||
)
|
||||
|
||||
// Hardware composes the fuel gauge and the AC-present line into one Sensor.
|
||||
//
|
||||
// Each device owns its own reacquire-on-error policy, so a fault on one bus
|
||||
// does not discard a healthy handle on the other.
|
||||
type Hardware struct {
|
||||
gauge *max17040.Gauge
|
||||
line *pld.Line
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
// NewHardware returns a Sensor backed by the real HAT. Neither device is opened
|
||||
// until the first Read.
|
||||
func NewHardware(i2cDevice, gpioChip string, gpioLine int) *Hardware {
|
||||
return &Hardware{
|
||||
gauge: max17040.New(i2cDevice),
|
||||
line: pld.New(gpioChip, gpioLine),
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// Read returns a complete sample or an error. It never returns a partially
|
||||
// populated Reading: a sample that is missing a channel cannot be rendered or
|
||||
// acted on, so there is nothing useful to pass upwards.
|
||||
//
|
||||
// Both channels are attempted even when the first fails, so one tick reports
|
||||
// every fault rather than hiding the second behind the first.
|
||||
func (h *Hardware) Read() (Reading, error) {
|
||||
acPresent, acErr := h.line.Read()
|
||||
volts, soc, batErr := h.gauge.Read()
|
||||
if err := errors.Join(acErr, batErr); err != nil {
|
||||
return Reading{}, err
|
||||
}
|
||||
return Reading{
|
||||
ACPresent: acPresent,
|
||||
Volts: volts,
|
||||
SOC: soc,
|
||||
At: h.now(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close releases both devices, reporting every failure.
|
||||
func (h *Hardware) Close() error {
|
||||
return errors.Join(h.line.Close(), h.gauge.Close())
|
||||
}
|
||||
Reference in New Issue
Block a user