Skip to content

Commit

Permalink
Mqtt: improve publishing structs (#18171)
Browse files Browse the repository at this point in the history
  • Loading branch information
andig authored Jan 11, 2025
1 parent 9ae0f1a commit 36f0005
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 29 deletions.
10 changes: 10 additions & 0 deletions server/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}
9 changes: 7 additions & 2 deletions server/mqtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}

Expand Down
116 changes: 89 additions & 27 deletions server/mqtt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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")
}

0 comments on commit 36f0005

Please sign in to comment.