Initila commit
Build Docker image on push / docker (push) Successful in 22s

This commit is contained in:
2026-08-18 00:46:59 +02:00
commit 9db7aa2560
28 changed files with 2823 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
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)
}
}