Files

157 lines
5.2 KiB
Go

package ha
import (
"encoding/json"
"reflect"
"testing"
"git.i3omb.com/gronod/ha-n95-local-control/internal/config"
"git.i3omb.com/gronod/ha-n95-local-control/internal/robot"
)
func testConfig() config.Config {
return config.Config{
MQTTBase: "ecovacs",
HADiscoveryPrefix: "homeassistant",
MQTTClientID: "n95-test",
}
}
func discoveryMap(t *testing.T, cfg config.Config, serial string) map[string]any {
t.Helper()
var doc map[string]any
if err := json.Unmarshal(Discovery(cfg, serial), &doc); err != nil {
t.Fatalf("discovery JSON invalid: %v", err)
}
return doc
}
func TestDiscoveryJSONOmitsPlatformAndBattery(t *testing.T) {
cfg := testConfig()
serial := "SER123"
payload := Discovery(cfg, serial)
want := `{"name":"Deebot N95","unique_id":"ecovacs_SER123",` +
`"command_topic":"ecovacs/SER123/command",` +
`"set_fan_speed_topic":"ecovacs/SER123/set_fan_speed",` +
`"send_command_topic":"ecovacs/SER123/send_command",` +
`"state_topic":"ecovacs/SER123/state",` +
`"json_attributes_topic":"ecovacs/SER123/json_attributes",` +
`"availability_topic":"ecovacs/SER123/availability",` +
`"payload_available":"online","payload_not_available":"offline",` +
`"fan_speed_list":["standard","strong"],` +
`"supported_features":["start","stop","return_home","status","locate","clean_spot","fan_speed","send_command"],` +
`"device":{"identifiers":["SER123"],"manufacturer":"Ecovacs","model":"Deebot N95 (wukong/155)","serial_number":"SER123"}}`
if string(payload) != want {
t.Fatalf("discovery payload mismatch\ngot: %s\nwant: %s", payload, want)
}
doc := discoveryMap(t, cfg, serial)
if _, ok := doc["platform"]; ok {
t.Fatal("discovery must not contain platform")
}
device, ok := doc["device"].(map[string]any)
if !ok {
t.Fatal("device must be an object")
}
if _, ok := device["connections"]; ok {
t.Fatal("device must not contain connections")
}
for _, banned := range []string{"battery", "pause", "clean_segments"} {
if _, ok := doc[banned]; ok {
t.Fatalf("discovery must not contain %s", banned)
}
}
}
func TestDiscoveryRetainedPrefix(t *testing.T) {
cfg := testConfig()
cfg.HADiscoveryPrefix = "ha2"
cfg.MQTTBase = "vac"
serial := "ABC999"
if got, want := DiscoveryTopic(cfg, serial), "ha2/vacuum/ecovacs_ABC999/config"; got != want {
t.Fatalf("DiscoveryTopic = %q, want %q", got, want)
}
doc := discoveryMap(t, cfg, serial)
if got := doc["unique_id"]; got != "ecovacs_ABC999" {
t.Fatalf("unique_id = %v; object id must keep ecovacs_{serial}", got)
}
if got := doc["command_topic"]; got != "vac/ABC999/command" {
t.Fatalf("command_topic = %v; must use MQTT_BASE", got)
}
}
func TestDiscoveryOmitsUnverifiedFeatures(t *testing.T) {
doc := discoveryMap(t, testConfig(), "SER1")
features, ok := doc["supported_features"].([]any)
if !ok {
t.Fatal("supported_features must be an array")
}
want := []string{"start", "stop", "return_home", "status", "locate", "clean_spot", "fan_speed", "send_command"}
var got []string
for _, f := range features {
got = append(got, f.(string))
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("supported_features = %v, want %v", got, want)
}
for _, banned := range []string{"battery", "pause", "clean_segments"} {
for _, f := range got {
if f == banned {
t.Fatalf("unverified feature %q advertised", banned)
}
}
}
}
func TestDiscoveryFanSpeedList(t *testing.T) {
doc := discoveryMap(t, testConfig(), "SER1")
list, ok := doc["fan_speed_list"].([]any)
if !ok || len(list) != 2 || list[0] != "standard" || list[1] != "strong" {
t.Fatalf("fan_speed_list = %v, want [standard strong]", doc["fan_speed_list"])
}
if got := robot.NewSnapshot().State.FanSpeed; got != "standard" {
t.Fatalf("initial fan_speed = %q, want standard", got)
}
}
func TestCommandMappingTable(t *testing.T) {
for _, tc := range []struct {
suffix, payload string
want robot.Command
}{
{"command", "start", robot.Command{Name: "start"}},
{"command", "stop", robot.Command{Name: "stop"}},
{"command", "return_to_base", robot.Command{Name: "return_to_base"}},
{"command", "clean_spot", robot.Command{Name: "clean_spot"}},
{"command", "locate", robot.Command{Name: "locate"}},
{"command", "pause", robot.Command{Reject: "pause"}},
{"command", "garbage", robot.Command{Reject: "garbage"}},
{"set_fan_speed", "standard", robot.Command{Name: "set_fan_speed", Args: map[string]string{"speed": "standard"}}},
{"set_fan_speed", "strong", robot.Command{Name: "set_fan_speed", Args: map[string]string{"speed": "strong"}}},
{"set_fan_speed", "turbo", robot.Command{Reject: "turbo"}},
{"unknown_suffix", "x", robot.Command{Reject: "unsupported"}},
} {
got := Command(tc.suffix, []byte(tc.payload))
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("Command(%q, %q) = %+v, want %+v", tc.suffix, tc.payload, got, tc.want)
}
}
}
func TestSendCommandCopiesPayload(t *testing.T) {
in := []byte(`{"command":"move"}`)
cmd := Command("send_command", in)
if cmd.Name != "send_command" {
t.Fatalf("Name = %q, want send_command", cmd.Name)
}
if string(cmd.Payload) != string(in) {
t.Fatalf("Payload = %q, want %q", cmd.Payload, in)
}
in[0] = 'X'
if cmd.Payload[0] == 'X' {
t.Fatal("Payload aliases the input slice; must be a copy")
}
}