generated from rmcguire/go-server-with-otel
136 lines
5.3 KiB
Go
136 lines
5.3 KiB
Go
package econetmetrics
|
|
|
|
import (
|
|
"context"
|
|
|
|
"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
|
|
// that snapshots client.Devices() once per scrape (cheap, in-memory).
|
|
func (c *Collector) registerLive() error {
|
|
b := &gaugeBuilder{m: c.meter}
|
|
g := &liveGauges{
|
|
setpoint: b.i64("econet_setpoint_fahrenheit", "Target water temperature (°F)"),
|
|
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_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 state update"),
|
|
info: b.i64("econet_device_info", "Device metadata (value is always 1)"),
|
|
}
|
|
if b.err != nil {
|
|
return b.err
|
|
}
|
|
_, err := c.meter.RegisterCallback(func(_ context.Context, o metric.Observer) error {
|
|
for _, d := range c.client.Devices() {
|
|
g.observe(o, d)
|
|
}
|
|
return nil
|
|
}, b.obs...)
|
|
return err
|
|
}
|
|
|
|
// 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{
|
|
energy: b.f64("econet_energy_usage_kwh", "Energy used today (kWh)"),
|
|
cost: b.f64("econet_energy_cost_dollars", "Energy cost today in US dollars (kWh * costPerKWH)"),
|
|
water: b.f64("econet_water_usage_gallons", "Water used today (gallons)"),
|
|
}
|
|
if b.err != nil {
|
|
return b.err
|
|
}
|
|
_, err := c.meter.RegisterCallback(func(_ context.Context, o metric.Observer) error {
|
|
for _, d := range c.client.Devices() {
|
|
g.observe(o, d, c.client.CostPerKWH())
|
|
}
|
|
return nil
|
|
}, b.obs...)
|
|
return err
|
|
}
|
|
|
|
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 *econetclient.Device) {
|
|
set := metric.WithAttributes(deviceAttrs(d)...)
|
|
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)...))
|
|
}
|
|
|
|
type usageGauges struct {
|
|
energy, cost, water metric.Float64ObservableGauge
|
|
}
|
|
|
|
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, 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 *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),
|
|
}
|
|
}
|
|
|
|
func infoAttrs(d *econetclient.Device) []attribute.KeyValue {
|
|
return append(deviceAttrs(d),
|
|
attribute.String("econet.type", d.Type),
|
|
attribute.String("econet.generic_type", d.GenericType),
|
|
attribute.String("econet.mode", d.Mode),
|
|
)
|
|
}
|
|
|
|
func b2i(b bool) int64 {
|
|
if b {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|