remove rheemcloud pkg, implement against rest api only
Build and Publish / helm-release (push) Has been skipped
Build and Publish / go-binaries (push) Has been skipped
Build and Publish / container-images (push) Has been skipped
Build and Publish / check-chart (push) Successful in 12s

This commit is contained in:
2026-07-05 16:59:53 -04:00
parent dcdf5e4230
commit 047f7c233d
15 changed files with 822 additions and 233 deletions
+42 -39
View File
@@ -3,9 +3,10 @@ package econetmetrics
import (
"context"
rheemcloud "github.com/kevinburke/rheemcloud-go"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"gitea.libretechconsulting.com/rmcguire/econet-exporter/pkg/econet/econetclient"
)
// registerLive wires the per-device live-state gauges to a single callback
@@ -17,13 +18,14 @@ func (c *Collector) registerLive() error {
setpointMin: b.i64("econet_setpoint_min_fahrenheit", "Minimum allowed setpoint (°F)"),
setpointMax: b.i64("econet_setpoint_max_fahrenheit", "Maximum allowed setpoint (°F)"),
connected: b.i64("econet_connected", "1 if the device is connected"),
running: b.i64("econet_running", "1 if the device is actively heating"),
running: b.i64("econet_running", "1 if the device is actively heating (running_state label names the stage, e.g. compressor-running)"),
mode: b.i64("econet_mode", "Operating mode enum: 1 for the current mode, 0 otherwise (see the mode label)"),
enabled: b.i64("econet_enabled", "1 if the device is enabled"),
away: b.i64("econet_away", "1 if away mode is on"),
hotWater: b.i64("econet_hot_water_availability_percent", "Hot water availability (%)"),
alerts: b.i64("econet_alert_count", "Number of active alerts"),
wifi: b.i64("econet_wifi_signal_db", "WiFi signal strength (dB)"),
lastUpdated: b.i64("econet_last_updated_seconds", "Unix time of last MQTT state update"),
lastUpdated: b.i64("econet_last_updated_seconds", "Unix time of last state update"),
info: b.i64("econet_device_info", "Device metadata (value is always 1)"),
}
if b.err != nil {
@@ -38,8 +40,8 @@ func (c *Collector) registerLive() error {
return err
}
// registerUsage wires the polled energy/water gauges, reading the cache the
// poller refreshes (the underlying REST calls are too slow for a callback).
// registerUsage wires the energy/water gauges, reading the usage totals the
// poller folds into each device snapshot.
func (c *Collector) registerUsage() error {
b := &gaugeBuilder{m: c.meter}
g := &usageGauges{
@@ -51,10 +53,8 @@ func (c *Collector) registerUsage() error {
return b.err
}
_, err := c.meter.RegisterCallback(func(_ context.Context, o metric.Observer) error {
c.mu.RLock()
defer c.mu.RUnlock()
for serial, u := range c.usage {
g.observe(o, serial, u, c.cfg.CostPerKWH)
for _, d := range c.client.Devices() {
g.observe(o, d, c.client.CostPerKWH())
}
return nil
}, b.obs...)
@@ -65,27 +65,30 @@ type liveGauges struct {
setpoint, setpointMin, setpointMax metric.Int64ObservableGauge
connected, running, enabled, away metric.Int64ObservableGauge
hotWater, alerts, wifi, lastUpdated metric.Int64ObservableGauge
mode metric.Int64ObservableGauge
info metric.Int64ObservableGauge
}
func (g *liveGauges) observe(o metric.Observer, d *rheemcloud.Device) {
func (g *liveGauges) observe(o metric.Observer, d *econetclient.Device) {
set := metric.WithAttributes(deviceAttrs(d)...)
min, max := d.SetpointLimits()
o.ObserveInt64(g.setpoint, int64(d.Setpoint()), set)
o.ObserveInt64(g.setpointMin, int64(min), set)
o.ObserveInt64(g.setpointMax, int64(max), set)
o.ObserveInt64(g.connected, b2i(d.Connected()), set)
o.ObserveInt64(g.running, b2i(d.Running()), set)
o.ObserveInt64(g.enabled, b2i(d.Enabled()), set)
o.ObserveInt64(g.away, b2i(d.Away()), set)
o.ObserveInt64(g.hotWater, int64(d.HotWaterAvailability()), set)
o.ObserveInt64(g.alerts, int64(d.AlertCount()), set)
o.ObserveInt64(g.wifi, int64(d.WiFiSignal()), set)
// LastUpdated is zero until the first MQTT update lands; skip it then
// rather than emit a nonsensical negative epoch.
o.ObserveInt64(g.setpoint, int64(d.Setpoint), set)
o.ObserveInt64(g.setpointMin, int64(d.SetpointMin), set)
o.ObserveInt64(g.setpointMax, int64(d.SetpointMax), set)
o.ObserveInt64(g.connected, b2i(d.Connected), set)
runSet := metric.WithAttributes(append(deviceAttrs(d), attribute.String("econet.running_state", d.RunningState))...)
o.ObserveInt64(g.running, b2i(d.Running), runSet)
o.ObserveInt64(g.enabled, b2i(d.Enabled), set)
o.ObserveInt64(g.away, b2i(d.Away), set)
o.ObserveInt64(g.hotWater, int64(d.HotWaterAvailability), set)
o.ObserveInt64(g.alerts, int64(d.AlertCount), set)
o.ObserveInt64(g.wifi, int64(d.WiFiSignal), set)
if !d.LastUpdated.IsZero() {
o.ObserveInt64(g.lastUpdated, d.LastUpdated.Unix(), set)
}
for _, m := range econetclient.KnownModes {
modeSet := metric.WithAttributes(append(deviceAttrs(d), attribute.String("econet.mode", m))...)
o.ObserveInt64(g.mode, b2i(m == d.Mode), modeSet)
}
o.ObserveInt64(g.info, 1, metric.WithAttributes(infoAttrs(d)...))
}
@@ -93,34 +96,34 @@ type usageGauges struct {
energy, cost, water metric.Float64ObservableGauge
}
func (g *usageGauges) observe(o metric.Observer, serial string, u usage, costPerKWH float64) {
set := metric.WithAttributes(attribute.String("econet.serial", serial))
o.ObserveFloat64(g.energy, u.energyKWH, metric.WithAttributes(
attribute.String("econet.serial", serial),
attribute.String("econet.energy_type", u.energyType),
func (g *usageGauges) observe(o metric.Observer, d *econetclient.Device, costPerKWH float64) {
serial := attribute.String("econet.serial", d.SerialNumber)
o.ObserveFloat64(g.energy, d.EnergyKWH, metric.WithAttributes(
serial,
attribute.String("econet.energy_type", d.EnergyType),
))
o.ObserveFloat64(g.water, u.waterGallons, set)
if u.energyType == "KWH" {
o.ObserveFloat64(g.cost, u.energyKWH*costPerKWH, set)
o.ObserveFloat64(g.water, d.WaterGallons, metric.WithAttributes(serial))
if d.EnergyType == "KWH" {
o.ObserveFloat64(g.cost, d.EnergyKWH*costPerKWH, metric.WithAttributes(serial))
}
}
// deviceAttrs are low-cardinality identity attributes safe for every series.
// They follow OTEL semantic-convention style (namespaced, dotted keys) and
// deliberately exclude anything that changes over time (e.g. timestamps).
func deviceAttrs(d *rheemcloud.Device) []attribute.KeyValue {
func deviceAttrs(d *econetclient.Device) []attribute.KeyValue {
return []attribute.KeyValue{
attribute.String("econet.serial", d.SerialNumber()),
attribute.String("econet.device_id", d.DeviceID()),
attribute.String("econet.friendly_name", d.FriendlyName()),
attribute.String("econet.serial", d.SerialNumber),
attribute.String("econet.device_id", d.DeviceID),
attribute.String("econet.friendly_name", d.FriendlyName),
}
}
func infoAttrs(d *rheemcloud.Device) []attribute.KeyValue {
func infoAttrs(d *econetclient.Device) []attribute.KeyValue {
return append(deviceAttrs(d),
attribute.String("econet.type", d.Type().String()),
attribute.String("econet.generic_type", d.GenericType()),
attribute.String("econet.mode", d.Mode().String()),
attribute.String("econet.type", d.Type),
attribute.String("econet.generic_type", d.GenericType),
attribute.String("econet.mode", d.Mode),
)
}