157 lines
5.2 KiB
Go
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")
|
|
}
|
|
}
|