package mqttclient import ( "testing" "git.coadcorp.com/nathan/invertergui/mk2driver" "github.com/stretchr/testify/assert" ) type fakeWriter struct { lastKind string lastID uint16 lastValue int16 lastSwitchState mk2driver.PanelSwitchState lastCurrentLimit *float64 lastStandby *bool err error } func (f *fakeWriter) WriteRAMVar(id uint16, value int16) error { f.lastKind = commandKindRAMVar f.lastID = id f.lastValue = value return f.err } func (f *fakeWriter) WriteSetting(id uint16, value int16) error { f.lastKind = commandKindSetting f.lastID = id f.lastValue = value return f.err } func (f *fakeWriter) SetPanelState(switchState mk2driver.PanelSwitchState, currentLimitA *float64) error { f.lastKind = commandKindPanel f.lastSwitchState = switchState f.lastCurrentLimit = currentLimitA return f.err } func (f *fakeWriter) SetStandby(standby bool) error { f.lastKind = commandKindStandby f.lastStandby = &standby return f.err } func Test_decodeWriteCommand(t *testing.T) { currentLimit := 16.5 tests := []struct { name string payload string check func(*testing.T, writeCommand) wantErr string }{ { name: "setting", payload: `{"request_id":"abc","kind":"setting","id":15,"value":-5}`, check: func(t *testing.T, got writeCommand) { assert.Equal(t, writeCommand{ RequestID: "abc", Kind: commandKindSetting, ID: 15, Value: -5, }, got) }, }, { name: "ram_var alias from type", payload: `{"type":"ramvar","id":2,"value":7}`, check: func(t *testing.T, got writeCommand) { assert.Equal(t, writeCommand{ Kind: commandKindRAMVar, ID: 2, Value: 7, }, got) }, }, { name: "panel state", payload: `{"kind":"panel_state","switch":"on","current_limit":16.5}`, check: func(t *testing.T, got writeCommand) { assert.Equal(t, commandKindPanel, got.Kind) assert.True(t, got.HasSwitch) assert.Equal(t, mk2driver.PanelSwitchOn, got.SwitchState) assert.Equal(t, "on", got.SwitchName) if assert.NotNil(t, got.CurrentLimitA) { assert.Equal(t, currentLimit, *got.CurrentLimitA) } }, }, { name: "panel current limit only", payload: `{"kind":"panel_state","current_limit":12.0}`, check: func(t *testing.T, got writeCommand) { assert.Equal(t, commandKindPanel, got.Kind) assert.False(t, got.HasSwitch) assert.Nil(t, got.Standby) if assert.NotNil(t, got.CurrentLimitA) { assert.Equal(t, 12.0, *got.CurrentLimitA) } }, }, { name: "standby bool", payload: `{"kind":"standby","standby":true}`, check: func(t *testing.T, got writeCommand) { assert.Equal(t, commandKindStandby, got.Kind) if assert.NotNil(t, got.Standby) { assert.True(t, *got.Standby) } }, }, { name: "standby using value string", payload: `{"kind":"standby","value":"OFF"}`, check: func(t *testing.T, got writeCommand) { assert.Equal(t, commandKindStandby, got.Kind) if assert.NotNil(t, got.Standby) { assert.False(t, *got.Standby) } }, }, { name: "missing id", payload: `{"kind":"setting","value":1}`, wantErr: `missing required field "id"`, }, { name: "missing panel switch and current limit", payload: `{"kind":"panel_state"}`, wantErr: `missing required field "switch"`, }, { name: "invalid standby", payload: `{"kind":"standby","value":"banana"}`, wantErr: `field "standby" must be true/false`, }, { name: "invalid kind", payload: `{"kind":"unknown","id":1,"value":1}`, wantErr: `unsupported write command kind "unknown"`, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := decodeWriteCommand([]byte(tt.payload)) if tt.wantErr != "" { assert.Error(t, err) assert.ErrorContains(t, err, tt.wantErr) return } assert.NoError(t, err) tt.check(t, got) }) } } func Test_executeWriteCommand(t *testing.T) { limit := 8.0 standby := true tests := []struct { name string cmd writeCommand want string }{ { name: "setting", cmd: writeCommand{ Kind: commandKindSetting, ID: 9, Value: 2, }, want: commandKindSetting, }, { name: "ram_var", cmd: writeCommand{ Kind: commandKindRAMVar, ID: 3, Value: -1, }, want: commandKindRAMVar, }, { name: "panel_state", cmd: writeCommand{ Kind: commandKindPanel, HasSwitch: true, SwitchState: mk2driver.PanelSwitchInverterOnly, CurrentLimitA: &limit, }, want: commandKindPanel, }, { name: "standby", cmd: writeCommand{ Kind: commandKindStandby, Standby: &standby, }, want: commandKindStandby, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { writer := &fakeWriter{} err := executeWriteCommand(writer, tt.cmd) assert.NoError(t, err) assert.Equal(t, tt.want, writer.lastKind) switch tt.want { case commandKindPanel: assert.Equal(t, tt.cmd.SwitchState, writer.lastSwitchState) if assert.NotNil(t, writer.lastCurrentLimit) { assert.Equal(t, *tt.cmd.CurrentLimitA, *writer.lastCurrentLimit) } case commandKindStandby: if assert.NotNil(t, writer.lastStandby) { assert.Equal(t, *tt.cmd.Standby, *writer.lastStandby) } default: assert.Equal(t, tt.cmd.ID, writer.lastID) assert.Equal(t, tt.cmd.Value, writer.lastValue) } }) } } func Test_buildHADiscoveryDefinitions(t *testing.T) { cfg := Config{ Topic: "invertergui/updates", CommandTopic: "invertergui/settings/set", HomeAssistant: HomeAssistantConfig{ Enabled: true, DiscoveryPrefix: "homeassistant", NodeID: "victron_main", DeviceName: "Shed Victron", }, } definitions := buildHADiscoveryDefinitions(cfg) assert.NotEmpty(t, definitions) var panelMode *haDiscoveryDefinition var panelCurrentLimit *haDiscoveryDefinition var panelStandby *haDiscoveryDefinition var batteryVoltage *haDiscoveryDefinition for i := range definitions { def := &definitions[i] if def.Component == "select" && def.ObjectID == "remote_panel_mode" { panelMode = def } if def.Component == "number" && def.ObjectID == "remote_panel_current_limit" { panelCurrentLimit = def } if def.Component == "switch" && def.ObjectID == "remote_panel_standby" { panelStandby = def } if def.Component == "sensor" && def.ObjectID == "battery_voltage" { batteryVoltage = def } } if assert.NotNil(t, panelMode) { assert.Equal(t, cfg.CommandTopic, panelMode.Config["command_topic"]) assert.Equal(t, haPanelSwitchStateTopic(cfg), panelMode.Config["state_topic"]) } if assert.NotNil(t, panelCurrentLimit) { assert.Equal(t, cfg.CommandTopic, panelCurrentLimit.Config["command_topic"]) assert.Equal(t, haCurrentLimitStateTopic(cfg), panelCurrentLimit.Config["state_topic"]) } if assert.NotNil(t, panelStandby) { assert.Equal(t, cfg.CommandTopic, panelStandby.Config["command_topic"]) assert.Equal(t, haStandbyStateTopic(cfg), panelStandby.Config["state_topic"]) } if assert.NotNil(t, batteryVoltage) { assert.Equal(t, cfg.Topic, batteryVoltage.Config["state_topic"]) } } func Test_panelStateCacheResolvePanelCommand(t *testing.T) { cache := &panelStateCache{} _, err := cache.resolvePanelCommand(writeCommand{ Kind: commandKindPanel, CurrentLimitA: float64Ptr(12), }) assert.Error(t, err) cache.remember(writeCommand{ Kind: commandKindPanel, HasSwitch: true, SwitchState: mk2driver.PanelSwitchOn, SwitchName: "on", }) resolved, err := cache.resolvePanelCommand(writeCommand{ Kind: commandKindPanel, CurrentLimitA: float64Ptr(10), }) assert.NoError(t, err) assert.True(t, resolved.HasSwitch) assert.Equal(t, mk2driver.PanelSwitchOn, resolved.SwitchState) assert.Equal(t, "on", resolved.SwitchName) } func float64Ptr(in float64) *float64 { return &in } func Test_normalizeID(t *testing.T) { assert.Equal(t, "victron_main_01", normalizeID("Victron Main #01")) assert.Equal(t, "inverter-gui", normalizeID(" inverter-gui ")) assert.Equal(t, "", normalizeID(" ")) }