lutron-control/config_test.go
2026-06-28 08:32:22 -05:00

115 lines
3.5 KiB
Go

package main
import (
"testing"
"github.com/kkyr/fig"
)
// TestExampleConfigValidates ensures the shipped config.example.yaml parses and
// passes cross-field validation, so the documented options stay in sync with the
// config structs.
func TestExampleConfigValidates(t *testing.T) {
cfg := &Config{Log: &LogConfig{}}
if err := fig.Load(cfg, fig.File("config.example.yaml"), fig.Dirs(".")); err != nil {
t.Fatalf("parse config.example.yaml: %s", err)
}
if err := cfg.Validate(); err != nil {
t.Fatalf("validate config.example.yaml: %s", err)
}
}
// baseConfig returns a minimal valid configuration with one serial device and an
// MQTT source, ready for per-test mutation.
func baseConfig() *Config {
return &Config{
Devices: []*DeviceConfig{{
Name: "grafik-eye",
Transport: "serial",
Serial: SerialConfig{Device: "/dev/ttyUSB0"},
Zones: 6,
}},
Sources: []*SourceConfig{{
Name: "home-assistant",
Type: "mqtt",
Device: "grafik-eye",
MQTT: MQTTConfig{Broker: "127.0.0.1", Topic: "lutron/qse-nwk"},
}},
}
}
// TestValidateMQTTLights verifies a light controlling zones 1-6 validates and that
// a zone outside the device's range is rejected.
func TestValidateMQTTLights(t *testing.T) {
c := baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{
{Name: "All Zones", Topic: "lutron/qse-nwk/all", Zones: []int{1, 2, 3, 4, 5, 6}},
}
if err := c.Validate(); err != nil {
t.Fatalf("valid lights config rejected: %s", err)
}
// A zone beyond the device's 6 zones must be rejected.
c.Sources[0].MQTT.Lights[0].Zones = []int{6, 7}
if err := c.Validate(); err == nil {
t.Error("expected error for out-of-range zone")
}
}
// TestValidateMQTTLightRequirements verifies per-light topic, zone-set, and
// duplicate-topic constraints.
func TestValidateMQTTLightRequirements(t *testing.T) {
// A light without a topic is rejected.
c := baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{{Zones: []int{1}}}
if err := c.Validate(); err == nil {
t.Error("expected error for missing light topic")
}
// A light with no zones is rejected.
c = baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{{Topic: "lutron/a"}}
if err := c.Validate(); err == nil {
t.Error("expected error for light with no zones")
}
// Two lights sharing a topic are rejected.
c = baseConfig()
c.Sources[0].MQTT.Lights = []MQTTLightConfig{
{Topic: "lutron/a", Zones: []int{1}},
{Topic: "lutron/a", Zones: []int{2}},
}
if err := c.Validate(); err == nil {
t.Error("expected error for duplicate light topic")
}
}
// TestNewMQTTSourceSynthesizesLight verifies that, with no lights configured, a
// single light over every zone is synthesized from the base topic.
func TestNewMQTTSourceSynthesizesLight(t *testing.T) {
cfg := &SourceConfig{
Name: "home-assistant",
Type: "mqtt",
MQTT: MQTTConfig{Broker: "127.0.0.1", Topic: "lutron/qse-nwk", DeviceName: "Lutron"},
}
dev := NewDevice(&DeviceConfig{Name: "grafik-eye", Zones: 6})
binding := dev.RegisterSource(cfg.Name, cfg.Priority, 0, cfg.Fade)
s := newMQTTSource(cfg, dev, binding)
if len(s.lights) != 1 {
t.Fatalf("expected 1 synthesized light, got %d", len(s.lights))
}
l := s.lights[0]
if l.topic != "lutron/qse-nwk" || l.topicSet != "lutron/qse-nwk/set" {
t.Errorf("unexpected light topics: %q / %q", l.topic, l.topicSet)
}
want := []int{1, 2, 3, 4, 5, 6}
if len(l.zones) != len(want) {
t.Fatalf("zones = %v, want %v", l.zones, want)
}
for i, z := range want {
if l.zones[i] != z {
t.Errorf("zones[%d] = %d, want %d", i, l.zones[i], z)
}
}
}