Skip to content

Commit

Permalink
Add Decoder support for TEKTELIC Kona All-in-One Home Sensor
Browse files Browse the repository at this point in the history
- Update `pkg/decode`, unit tests, and decoder unit and integration tests.
- Sync `vendor/` for updated dependencies.
- All test variations pass.
  • Loading branch information
bconway committed Jan 31, 2024
1 parent 025cd70 commit 7f58a7a
Show file tree
Hide file tree
Showing 48 changed files with 1,386 additions and 450 deletions.
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ require (
github.com/NYTimes/gziphandler v1.1.1
github.com/chirpstack/chirpstack/api/go/v4 v4.6.0
github.com/eclipse/paho.mqtt.golang v1.4.3
github.com/expr-lang/expr v1.15.8
github.com/expr-lang/expr v1.16.0
github.com/google/uuid v1.6.0
github.com/gregdel/pushover v1.3.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1
github.com/jackc/pgx/v5 v5.5.2
github.com/jellydator/ttlcache/v3 v3.1.1
github.com/mennanov/fmutils v0.2.1
github.com/nsqio/go-nsq v1.1.0
github.com/redis/go-redis/v9 v9.4.0
github.com/smira/go-statsd v1.3.3
github.com/stretchr/testify v1.8.4
github.com/thingspect/proto/go v1.1.1
github.com/thingspect/proto/go v1.1.2
go.uber.org/mock v0.4.0
golang.org/x/crypto v0.18.0
google.golang.org/grpc v1.61.0
Expand All @@ -41,8 +41,8 @@ require (
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
24 changes: 12 additions & 12 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,8 @@ github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A=
github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew=
github.com/expr-lang/expr v1.15.8 h1:FL8+d3rSSP4tmK9o+vKfSMqqpGL8n15pEPiHcnBpxoI=
github.com/expr-lang/expr v1.15.8/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/expr-lang/expr v1.16.0 h1:BQabx+PbjsL2PEQwkJ4GIn3CcuUh8flduHhJ0lHjWwE=
github.com/expr-lang/expr v1.16.0/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
Expand Down Expand Up @@ -530,8 +530,8 @@ github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZH
github.com/gregdel/pushover v1.3.0 h1:CewbxqsThoN/1imgwkDKFkRkltaQMoyBV0K9IquQLtw=
github.com/gregdel/pushover v1.3.0/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
Expand Down Expand Up @@ -585,8 +585,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/thingspect/proto/go v1.1.1 h1:v1C9dK7i0VPf0ZCO5TRmL1LV4wPS5ouGXjBKpvMto0c=
github.com/thingspect/proto/go v1.1.1/go.mod h1:6AGf7laq7xTrkNGxTvblyGiq+ip3BqkYYosXjhh7uIY=
github.com/thingspect/proto/go v1.1.2 h1:Gzyk6Nt8S44vOOx4FqFFdVXdVuJVzJdfhykI0neaQUc=
github.com/thingspect/proto/go v1.1.2/go.mod h1:kZNThqULGn3iOcOMTL7RScdQK6FTkfi08E2YhRvmtHM=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
Expand Down Expand Up @@ -1051,12 +1051,12 @@ google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZV
google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg=
google.golang.org/genproto v0.0.0-20230202175211-008b39050e57/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ=
google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80 h1:Lj5rbfG876hIAYFjqiJnPHfhXbv+nzTWfm04Fg/XSVU=
google.golang.org/genproto/googleapis/api v0.0.0-20240123012728-ef4313101c80/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo=
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8=
google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
Expand Down
37 changes: 37 additions & 0 deletions internal/atlas-decoder/decoder/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,43 @@ func TestDecodeMessages(t *testing.T) {
},
},
},
{
&message.DecoderIn{
UniqId: uniqID, Data: []byte{
0x03, 0x67, 0x00, 0xc4, 0x04, 0x68, 0x7f, 0x00, 0xff, 0x01,
0x38,
}, Ts: now, TraceId: traceID[:],
}, api.Decoder_TEKTELIC_HOME, []*message.ValidatorIn{
{
Point: &common.DataPoint{
UniqId: uniqID, Attr: "temp_c",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 19.6},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
{
Point: &common.DataPoint{
UniqId: uniqID, Attr: "temp_f",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 67.3},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
{
Point: &common.DataPoint{
UniqId: uniqID, Attr: "humidity_pct",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 63.5},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
{
Point: &common.DataPoint{
UniqId: uniqID, Attr: "battery_v",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 3.12},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
},
},
}

for _, test := range tests {
Expand Down
44 changes: 44 additions & 0 deletions internal/atlas-decoder/test/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ func TestDecodeMessages(t *testing.T) {
t.Logf("createCO2Dev, err: %+v, %v", createCO2Dev, err)
require.NoError(t, err)

homeDev := random.Device("dec", createOrg.GetId())
homeDev.Status = api.Status_ACTIVE
homeDev.Decoder = api.Decoder_TEKTELIC_HOME
createHomeDev, err := globalDevDAO.Create(ctx, homeDev)
t.Logf("createHomeDev, err: %+v, %v", createHomeDev, err)
require.NoError(t, err)

tests := []struct {
inp *message.DecoderIn
res []*message.ValidatorIn
Expand Down Expand Up @@ -126,6 +133,43 @@ func TestDecodeMessages(t *testing.T) {
},
},
},
{
&message.DecoderIn{
UniqId: homeDev.GetUniqId(), Data: []byte{
0x03, 0x67, 0x00, 0xc4, 0x04, 0x68, 0x7f, 0x00, 0xff, 0x01,
0x38,
}, Ts: now, TraceId: traceID[:],
}, []*message.ValidatorIn{
{
Point: &common.DataPoint{
UniqId: homeDev.GetUniqId(), Attr: "temp_c",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 19.6},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
{
Point: &common.DataPoint{
UniqId: homeDev.GetUniqId(), Attr: "temp_f",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 67.3},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
{
Point: &common.DataPoint{
UniqId: homeDev.GetUniqId(), Attr: "humidity_pct",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 63.5},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
{
Point: &common.DataPoint{
UniqId: homeDev.GetUniqId(), Attr: "battery_v",
ValOneof: &common.DataPoint_Fl64Val{Fl64Val: 3.12},
Ts: now, TraceId: traceID.String(),
}, SkipToken: true,
},
},
},
}

for _, test := range tests {
Expand Down
13 changes: 13 additions & 0 deletions pkg/decode/c_to_f.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package decode

import (
"math"
)

// CToF converts a temperature from Celsius to Fahrenheit, rounded to one
// decimal digit.
func CToF(tempC float64) float64 {
tempF := tempC*9/5 + 32

return math.Round(tempF*10) / 10
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//go:build !integration

package globalsat
package decode

import (
"fmt"
Expand Down Expand Up @@ -30,7 +30,7 @@ func TestCToF(t *testing.T) {
t.Run(fmt.Sprintf("Can convert %+v", lTest), func(t *testing.T) {
t.Parallel()

res := cToF(lTest.inp)
res := CToF(lTest.inp)
t.Logf("res: %v", res)
require.InDelta(t, lTest.res, res, epsilon)
})
Expand Down
12 changes: 0 additions & 12 deletions pkg/decode/globalsat/globalsat.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,2 @@
// Package globalsat provides parse functions for GlobalSat sensors.
package globalsat

import (
"math"
)

// cToF converts a temperature from Celsius to Fahrenheit, rounded to one
// decimal digit.
func cToF(tempC float64) float64 {
tempF := tempC*9/5 + 32

return math.Round(tempF*10) / 10
}
3 changes: 2 additions & 1 deletion pkg/decode/globalsat/ls11x.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ func ls11x(body []byte) ([]*decode.Point, error) {
// Parse temperature, rounded to one decimal digit.
tempC := float64(int16(binary.BigEndian.Uint16(body[1:3]))) / 100
msgs := []*decode.Point{{Attr: "temp_c", Value: math.Round(tempC*10) / 10}}
msgs = append(msgs, &decode.Point{Attr: "temp_f", Value: cToF(tempC)})
msgs = append(msgs,
&decode.Point{Attr: "temp_f", Value: decode.CToF(tempC)})

// Parse humidity.
hum := float64(binary.BigEndian.Uint16(body[3:5])) / 100
Expand Down
2 changes: 1 addition & 1 deletion pkg/decode/radiobridge/reset.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

const identReset = 0x00

// reset parses an Reset (boot) payload from a []byte according to the spec.
// reset parses a Reset (boot) payload from a []byte according to the spec.
// Reset payloads are used to confirm device settings on boot.
func reset(body []byte) ([]*decode.Point, error) {
const fwFormat = 0b10000000
Expand Down
2 changes: 2 additions & 0 deletions pkg/decode/registry/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/thingspect/atlas/pkg/decode"
"github.com/thingspect/atlas/pkg/decode/globalsat"
"github.com/thingspect/atlas/pkg/decode/radiobridge"
"github.com/thingspect/atlas/pkg/decode/tektelic"
"github.com/thingspect/proto/go/api"
)

Expand All @@ -34,6 +35,7 @@ func New() *Registry {
api.Decoder_GLOBALSAT_CO2: globalsat.CO2,
api.Decoder_GLOBALSAT_CO: globalsat.CO,
api.Decoder_GLOBALSAT_PM25: globalsat.PM25,
api.Decoder_TEKTELIC_HOME: tektelic.Home,
},
}
}
Expand Down
8 changes: 7 additions & 1 deletion pkg/decode/registry/registry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,17 @@ func TestDecode(t *testing.T) {
{Attr: "open", Value: true},
}, nil},
{api.Decoder_GLOBALSAT_CO2, "01096113950292", []*decode.Point{
{Attr: "temp_c", Value: float64(24)},
{Attr: "temp_c", Value: 24.0},
{Attr: "temp_f", Value: 75.2},
{Attr: "humidity_pct", Value: 50.13},
{Attr: "co2_ppm", Value: int32(658)},
}, nil},
{api.Decoder_TEKTELIC_HOME, "036700c404687f00ff0138", []*decode.Point{
{Attr: "temp_c", Value: 19.6},
{Attr: "temp_f", Value: 67.3},
{Attr: "humidity_pct", Value: 63.5},
{Attr: "battery_v", Value: 3.12},
}, nil},
// Decoder function not found.
{api.Decoder(999), "", nil, fmt.Errorf("%w: 999", ErrNotFound)},
}
Expand Down
101 changes: 101 additions & 0 deletions pkg/decode/tektelic/channel_type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package tektelic

import (
"github.com/thingspect/atlas/pkg/decode"
)

const (
identChanMotion = 0x0a
identChanTempC = 0x03
identChanHumidity = 0x04
identChanBatteryV = 0x00
)

// chanMotion parses a Motion data channel from a []byte according to the spec
// and returns the points, the remaining bytes, and an error value.
func chanMotion(body []byte) ([]*decode.Point, []byte, error) {
// Motion data channel must be at least 3 bytes.
if len(body) < 3 {
return nil, nil, decode.ErrFormat("chanMotion", "bad length", body)
}

if body[0] != identChanMotion {
return nil, nil, decode.ErrFormat("chanMotion", "bad identifier", body)
}

// Parse motion.
motion, rem, err := typeDigital(body)
if err != nil {
return nil, nil, err
}

return []*decode.Point{{Attr: "motion", Value: motion}}, rem, nil
}

// chanTempC parses a Temperature data channel from a []byte according to the
// spec and returns the points, the remaining bytes, and an error value.
func chanTempC(body []byte) ([]*decode.Point, []byte, error) {
// Temperature data channel must be at least 4 bytes.
if len(body) < 4 {
return nil, nil, decode.ErrFormat("chanTempC", "bad length", body)
}

if body[0] != identChanTempC {
return nil, nil, decode.ErrFormat("chanTempC", "bad identifier", body)
}

// Parse temperature.
tempC, rem, err := typeTempC(body)
if err != nil {
return nil, nil, err
}

return []*decode.Point{
{Attr: "temp_c", Value: tempC},
{Attr: "temp_f", Value: decode.CToF(tempC)},
}, rem, nil
}

// chanHumidity parses a Humidity data channel from a []byte according to the
// spec and returns the points, the remaining bytes, and an error value.
func chanHumidity(body []byte) ([]*decode.Point, []byte, error) {
// Humidity data channel must be at least 3 bytes.
if len(body) < 3 {
return nil, nil, decode.ErrFormat("chanHumidity", "bad length", body)
}

if body[0] != identChanHumidity {
return nil, nil, decode.ErrFormat("chanHumidity", "bad identifier",
body)
}

// Parse humidity.
humidity, rem, err := typeHumidity(body)
if err != nil {
return nil, nil, err
}

return []*decode.Point{{Attr: "humidity_pct", Value: humidity}}, rem, nil
}

// chanBatteryV parses a Battery (V) data channel from a []byte according to the
// spec and returns the points, the remaining bytes, and an error value.
func chanBatteryV(body []byte) ([]*decode.Point, []byte, error) {
// Battery (V) data channel must be at least 4 bytes.
if len(body) < 4 {
return nil, nil, decode.ErrFormat("chanBatteryV", "bad length", body)
}

if body[0] != identChanBatteryV {
return nil, nil, decode.ErrFormat("chanBatteryV", "bad identifier",
body)
}

// Parse voltage.
voltage, rem, err := typeAnalogV(body)
if err != nil {
return nil, nil, err
}

return []*decode.Point{{Attr: "battery_v", Value: voltage}}, rem, nil
}
Loading

0 comments on commit 7f58a7a

Please sign in to comment.