diff --git a/server/helper.go b/server/helper.go index 881b6a9983..43cd12060a 100644 --- a/server/helper.go +++ b/server/helper.go @@ -3,7 +3,10 @@ package server import ( "fmt" "math" + "reflect" + "slices" "strconv" + "strings" ) // pass converts a simple api without return value to api with nil error return value @@ -22,3 +25,10 @@ func parseFloat(payload string) (float64, error) { } return f, err } + +// omitEmpty returns true if struct field is omitempty +func omitEmpty(f reflect.StructField) bool { + tag := f.Tag.Get("json") + values := strings.Split(tag, ",") + return slices.Contains(values, "omitempty") +} diff --git a/server/mqtt.go b/server/mqtt.go index d9f4f6920b..cddd70fd77 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -101,8 +101,13 @@ func (m *MQTT) publishComplex(topic string, retained bool, payload interface{}) // loop struct for i := 0; i < typ.NumField(); i++ { if f := typ.Field(i); f.IsExported() { - n := f.Name - m.publishComplex(fmt.Sprintf("%s/%s", topic, strings.ToLower(n[:1])+n[1:]), retained, val.Field(i).Interface()) + topic := fmt.Sprintf("%s/%s", topic, strings.ToLower(f.Name[:1])+f.Name[1:]) + + if val.Field(i).IsZero() && omitEmpty(f) { + m.publishSingleValue(topic, retained, nil) + } else { + m.publishComplex(topic, retained, val.Field(i).Interface()) + } } } diff --git a/server/mqtt_test.go b/server/mqtt_test.go index a7dd134791..264e8e47c8 100644 --- a/server/mqtt_test.go +++ b/server/mqtt_test.go @@ -2,12 +2,14 @@ package server import ( "math" + "slices" "strconv" "testing" "time" + "github.com/samber/lo" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" ) func TestMqttNaNInf(t *testing.T) { @@ -16,51 +18,111 @@ func TestMqttNaNInf(t *testing.T) { assert.Equal(t, "+Inf", m.encode(math.Inf(0)), "Inf not encoded as string") } +type measurement struct { + Power float64 `json:"power"` + Energy float64 `json:"energy,omitempty"` + Currents []float64 `json:"currents,omitempty"` + Controllable *bool `json:"controllable,omitempty"` +} + func TestPublishTypes(t *testing.T) { - var topics, payloads []string + suite.Run(t, new(mqttSuite)) +} - reset := func() { - topics = topics[:0] - payloads = payloads[:0] +type mqttSuite struct { + suite.Suite + *MQTT + topics, payloads []string +} + +func (suite *mqttSuite) publish(topic string, retained bool, payload interface{}) { + suite.MQTT.publish(topic, retained, payload) +} + +func (suite *mqttSuite) publisher(topic string, retained bool, payload string) { + if i := slices.Index(suite.topics, topic); i >= 0 { + suite.topics[i] = topic + suite.payloads[i] = payload + } else { + suite.topics = append(suite.topics, topic) + suite.payloads = append(suite.payloads, payload) } +} - m := &MQTT{ - publisher: func(topic string, retained bool, payload string) { - topics = append(topics, topic) - payloads = append(payloads, payload) - }, +func (suite *mqttSuite) SetupSuite() { + suite.MQTT = &MQTT{ + publisher: suite.publisher, } +} + +func (suite *mqttSuite) SetupTest() { + suite.topics = suite.topics[:0] + suite.payloads = suite.payloads[:0] +} +func (suite *mqttSuite) TestTime() { now := time.Now() - m.publish("test", false, now) - require.Len(t, topics, 1) - assert.Equal(t, strconv.FormatInt(now.Unix(), 10), payloads[0], "time not encoded as unix timestamp") - reset() + suite.publish("test", false, now) + suite.Require().Len(suite.topics, 1) + suite.Equal(strconv.FormatInt(now.Unix(), 10), suite.payloads[0], "time not encoded as unix timestamp") +} + +func (suite *mqttSuite) TestBool() { + suite.publish("test", false, false) + suite.Require().Len(suite.topics, 1) + suite.Equal("false", suite.payloads[0]) +} - m.publish("test", false, struct { +func (suite *mqttSuite) TestStruct() { + suite.publish("test", false, struct { Foo string }{ Foo: "bar", }) - assert.Equal(t, []string{"test/foo"}, topics, "struct mismatch") - assert.Equal(t, []string{"bar"}, payloads, "struct mismatch") - reset() + suite.Equal([]string{"test/foo"}, suite.topics, "topics") + suite.Equal([]string{"bar"}, suite.payloads, "payloads") +} +func (suite *mqttSuite) TestStructPointer() { i := 1 - m.publish("test", false, struct { + suite.publish("test", false, struct { Foo, Bar *int }{ Foo: &i, Bar: nil, }) - assert.Equal(t, []string{"test/foo", "test/bar"}, topics, "pointer mismatch") - assert.Equal(t, []string{"1", ""}, payloads, "pointer mismatch") - reset() + suite.Equal([]string{"test/foo", "test/bar"}, suite.topics, "topics") + suite.Equal([]string{"1", ""}, suite.payloads, "payloads") +} +func (suite *mqttSuite) TestSlice() { slice := []int{10, 20} - m.publish("test", false, slice) - require.Len(t, topics, 3) - assert.Equal(t, []string{"test", "test/1", "test/2"}, topics, "slice mismatch") - assert.Equal(t, []string{"2", "10", "20"}, payloads, "slice mismatch") - reset() + suite.publish("test", false, slice) + suite.Require().Len(suite.topics, 3) + suite.Equal([]string{"test", "test/1", "test/2"}, suite.topics, "topics") + suite.Equal([]string{"2", "10", "20"}, suite.payloads, "payloads") +} + +func (suite *mqttSuite) TestGrid() { + topics := []string{"test/power", "test/energy", "test/currents", "test/controllable"} + + suite.publish("test", false, measurement{}) + suite.Require().Len(suite.topics, 4) + suite.Equal(topics, suite.topics, "topics") + suite.Equal([]string{"0", "", "", ""}, suite.payloads, "payloads") + + suite.publish("test", false, measurement{Energy: 1}) + suite.Require().Len(suite.topics, 4) + suite.Equal(topics, suite.topics, "topics") + suite.Equal([]string{"0", "1", "", ""}, suite.payloads, "payloads") + + suite.publish("test", false, measurement{Controllable: lo.ToPtr(false)}) + suite.Require().Len(suite.topics, 4) + suite.Equal(topics, suite.topics, "topics") + suite.Equal([]string{"0", "", "", "false"}, suite.payloads, "payloads") + + suite.publish("test", false, measurement{Currents: []float64{1, 2, 3}}) + suite.Require().Len(suite.topics, 7) + suite.Equal([]string{"test/power", "test/energy", "test/currents", "test/controllable", "test/currents/1", "test/currents/2", "test/currents/3"}, suite.topics, "topics") + suite.Equal([]string{"0", "", "3", "", "1", "2", "3"}, suite.payloads, "payloads") }