package collector import ( "context" "encoding/json" "errors" "math" "testing" "github.com/prometheus/client_golang/prometheus" dto "github.com/prometheus/client_model/go" "lostak.dev/shelly-exporter/configuration" ) const plugSStatusFixture = `{ "wifi_sta":{"connected":true,"ssid":"lostak","ip":"192.168.0.30","rssi":-77}, "cloud":{"enabled":false,"connected":false}, "mqtt":{"connected":false}, "time":"23:52","serial":1,"has_update":false,"mac":"E8DB84BC666A", "relays":[{"ison":true,"has_timer":false,"overpower":false}], "meters":[{"power":166.00,"is_valid":true,"timestamp":1787010749,"counters":[168.129,167.929,167.771],"total":4188430}], "temperature":49.40,"overtemperature":false, "update":{"status":"unknown","has_update":false,"new_version":"","old_version":"20190516-073020/master@ea1b23db"}, "ram_total":50832,"ram_free":40188,"fs_size":233681,"fs_free":171182,"uptime":1512798 }` type statusClientFunc func(context.Context, any) error func (function statusClientFunc) GetStatus(ctx context.Context, destination any) error { return function(ctx, destination) } func fixtureClient(_ context.Context, destination any) error { return json.Unmarshal([]byte(plugSStatusFixture), destination) } func TestPlugSCollectorExposesStatusAndEnergyCounter(t *testing.T) { deviceCollector, err := NewDeviceCollector(configuration.DeviceConfiguration{ Name: "office", Product: "plug_s", }, statusClientFunc(fixtureClient)) if err != nil { t.Fatal(err) } registry := prometheus.NewPedanticRegistry() if err := registry.Register(deviceCollector); err != nil { t.Fatal(err) } families, err := registry.Gather() if err != nil { t.Fatalf("Gather() returned an unexpected error: %v", err) } energy := findMetricFamily(t, families, "shelly_meter_energy_watt_hours_total") if energy.GetType() != dto.MetricType_COUNTER { t.Fatalf("energy metric type = %s, want COUNTER", energy.GetType()) } value := energy.Metric[0].GetCounter().GetValue() want := 4188430.0 / 60.0 if math.Abs(value-want) > 0.000001 { t.Fatalf("energy counter = %f, want %f", value, want) } assertGauge(t, families, "shelly_up", 1) assertGauge(t, families, "shelly_meter_power_watts", 166) assertGauge(t, families, "shelly_temperature_celsius", 49.4) assertGauge(t, families, "shelly_relay_on", 1) } func TestPlugSCollectorReportsFailedScrapes(t *testing.T) { client := statusClientFunc(func(context.Context, any) error { return errors.New("unreachable") }) deviceCollector, err := NewDeviceCollector(configuration.DeviceConfiguration{ Name: "office", Product: "plug-s", }, client) if err != nil { t.Fatal(err) } registry := prometheus.NewRegistry() if err := registry.Register(deviceCollector); err != nil { t.Fatal(err) } for expectedErrors := 1.0; expectedErrors <= 2; expectedErrors++ { families, err := registry.Gather() if err != nil { t.Fatal(err) } assertGauge(t, families, "shelly_up", 0) errorsFamily := findMetricFamily(t, families, "shelly_scrape_errors_total") if value := errorsFamily.Metric[0].GetCounter().GetValue(); value != expectedErrors { t.Fatalf("scrape errors = %f, want %f", value, expectedErrors) } } } func TestGroupAllowsMultipleDevicesWithTheSameMetrics(t *testing.T) { group := make(Group, 0, 2) for _, name := range []string{"office", "kitchen"} { deviceCollector, err := NewDeviceCollector(configuration.DeviceConfiguration{ Name: name, Product: "plug_s", }, statusClientFunc(fixtureClient)) if err != nil { t.Fatal(err) } group = append(group, deviceCollector) } registry := prometheus.NewPedanticRegistry() if err := registry.Register(group); err != nil { t.Fatalf("Register() returned an unexpected error: %v", err) } families, err := registry.Gather() if err != nil { t.Fatalf("Gather() returned an unexpected error: %v", err) } up := findMetricFamily(t, families, "shelly_up") if len(up.Metric) != 2 { t.Fatalf("shelly_up has %d metrics, want 2", len(up.Metric)) } } func findMetricFamily(t *testing.T, families []*dto.MetricFamily, name string) *dto.MetricFamily { t.Helper() for _, family := range families { if family.GetName() == name { return family } } t.Fatalf("metric family %q not found", name) return nil } func assertGauge(t *testing.T, families []*dto.MetricFamily, name string, want float64) { t.Helper() family := findMetricFamily(t, families, name) if family.GetType() != dto.MetricType_GAUGE { t.Fatalf("%s type = %s, want GAUGE", name, family.GetType()) } if value := family.Metric[0].GetGauge().GetValue(); value != want { t.Fatalf("%s = %f, want %f", name, value, want) } }