forked from absmach/senml
-
Notifications
You must be signed in to change notification settings - Fork 0
/
senml.go
266 lines (239 loc) · 7.15 KB
/
senml.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
package senml
import (
"encoding/json"
"encoding/xml"
"errors"
"sort"
"github.com/fxamacker/cbor/v2"
)
const (
xmlns = "urn:ietf:params:xml:ns:senml"
defaultVersion = 10
)
// Format represents SenML message format.
type Format int
// Supported formats are JSON, XML, and CBOR.
const (
JSON Format = 1 + iota
XML
CBOR
)
var (
// ErrVersionChange indicates that records with different BaseVersion are present in Pack.
ErrVersionChange = errors.New("version change")
// ErrUnsupportedFormat indicates the wrong message format (format other than JSON, XML or CBOR).
ErrUnsupportedFormat = errors.New("unsupported format")
// ErrEmptyName indicates empty record name.
ErrEmptyName = errors.New("empty name")
// ErrBadChar indicates invalid char or that char is not allowed at the given position.
ErrBadChar = errors.New("invalid char")
// ErrTooManyValues indicates that there is more than one value field.
ErrTooManyValues = errors.New("more than one value in the record")
// ErrNoValues indicates that there is no value nor sum field present.
ErrNoValues = errors.New("no value or sum field found")
)
// Record represents one senML record.
type Record struct {
XMLName *bool `json:"-" xml:"senml" cbor:"-"`
Link string `json:"l,omitempty" xml:"l,attr,omitempty" cbor:"-"`
BaseName string `json:"bn,omitempty" xml:"bn,attr,omitempty" cbor:"-2,keyasint,omitempty"`
BaseTime float64 `json:"bt,omitempty" xml:"bt,attr,omitempty" cbor:"-3,keyasint,omitempty"`
BaseUnit string `json:"bu,omitempty" xml:"bu,attr,omitempty" cbor:"-4,keyasint,omitempty"`
BaseVersion uint `json:"bver,omitempty" xml:"bver,attr,omitempty" cbor:"-1,keyasint,omitempty"`
BaseValue float64 `json:"bv,omitempty" xml:"bv,attr,omitempty" cbor:"-5,keyasint,omitempty"`
BaseSum float64 `json:"bs,omitempty" xml:"bs,attr,omitempty" cbor:"-6,keyasint,omitempty"`
Name string `json:"n,omitempty" xml:"n,attr,omitempty" cbor:"0,keyasint,omitempty"`
Unit string `json:"u,omitempty" xml:"u,attr,omitempty" cbor:"1,keyasint,omitempty"`
Time float64 `json:"t,omitempty" xml:"t,attr,omitempty" cbor:"6,keyasint,omitempty"`
UpdateTime float64 `json:"ut,omitempty" xml:"ut,attr,omitempty" cbor:"7,keyasint,omitempty"`
Value *float64 `json:"v,omitempty" xml:"v,attr,omitempty" cbor:"2,keyasint,omitempty"`
StringValue *string `json:"vs,omitempty" xml:"vs,attr,omitempty" cbor:"3,keyasint,omitempty"`
DataValue *string `json:"vd,omitempty" xml:"vd,attr,omitempty" cbor:"8,keyasint,omitempty"`
BoolValue *bool `json:"vb,omitempty" xml:"vb,attr,omitempty" cbor:"4,keyasint,omitempty"`
Sum *float64 `json:"s,omitempty" xml:"s,attr,omitempty" cbor:"5,keyasint,omitempty"`
}
// Pack consists of SenML records array.
type Pack struct {
XMLName *bool `json:"-" xml:"sensml"`
Xmlns string `json:"-" xml:"xmlns,attr"`
Records []Record `xml:"senml"`
}
// Implement sort.Interface so that resolved recods can easily be sorted.
func (p *Pack) Len() int {
return len(p.Records)
}
func (p *Pack) Less(i, j int) bool {
return p.Records[i].Time < p.Records[j].Time
}
func (p *Pack) Swap(i, j int) {
p.Records[i], p.Records[j] = p.Records[j], p.Records[i]
}
// Decode takes a SenML message in the given format and parses it and decodes it
// into the returned SenML record.
func Decode(msg []byte, format Format) (Pack, error) {
var p Pack
switch format {
case JSON:
if err := json.Unmarshal(msg, &p.Records); err != nil {
return Pack{}, err
}
case XML:
if err := xml.Unmarshal(msg, &p); err != nil {
return Pack{}, err
}
p.Xmlns = xmlns
case CBOR:
if err := cbor.Unmarshal(msg, &p.Records); err != nil {
return Pack{}, err
}
default:
return Pack{}, ErrUnsupportedFormat
}
return p, Validate(p)
}
// Encode takes a SenML Pack and encodes it using the given format.
func Encode(p Pack, format Format) ([]byte, error) {
switch format {
case JSON:
return json.Marshal(p.Records)
case XML:
p.Xmlns = xmlns
return xml.Marshal(p)
case CBOR:
return cbor.Marshal(p.Records)
default:
return nil, ErrUnsupportedFormat
}
}
func replaceInvalidCharacters(name string) string {
out := []rune(name)
for pos, l := range name {
if (l < 'a' || l > 'z') && (l < 'A' || l > 'Z') && (l < '0' || l > '9') && (l != '-') && (l != ':') && (l != '.') && (l != '/') && (l != '_') {
out[pos] = '_'
}
}
return string(out)
}
// Normalize removes all the base values and expands records values with the base items.
// The base fields apply to the entries in the Record and also to all Records after
// it up to, but not including, the next Record that has that same base field.
func Normalize(p Pack) (Pack, error) {
for _, r := range p.Records {
r.Name = replaceInvalidCharacters(r.Name)
}
// Validate ensures that all the BaseVersions are equal.
if err := Validate(p); err != nil {
return Pack{}, err
}
records := make([]Record, len(p.Records))
var bname string
var btime float64
var bsum float64
var bunit string
for i, r := range p.Records {
if r.BaseTime != 0 {
btime = r.BaseTime
}
if r.BaseSum != 0 {
bsum = r.BaseSum
}
if r.BaseUnit != "" {
bunit = r.BaseUnit
}
if len(r.BaseName) > 0 {
bname = r.BaseName
}
r.Name = replaceInvalidCharacters(bname + r.Name)
r.Time = btime + r.Time
if r.Sum != nil {
*r.Sum = bsum + *r.Sum
}
if r.Unit == "" {
r.Unit = bunit
}
if r.Value != nil && r.BaseValue != 0 {
*r.Value = r.BaseValue + *r.Value
}
// If the version is default, it must not be present in resolved records.
// Validate method takes care that the version is the same on all the records.
if r.BaseVersion == defaultVersion {
r.BaseVersion = 0
}
// Remove Base Values from the Record.
r.BaseTime = 0
r.BaseValue = 0
r.BaseUnit = ""
r.BaseName = ""
r.BaseSum = 0
records[i] = r
}
p.Records = records
sort.Sort(&p)
return p, nil
}
// Validate validates SenML records.
func Validate(p Pack) error {
var bver uint
var bname string
var bsum float64
for _, r := range p.Records {
// Check if version is the same for all the records.
if bver == 0 && r.BaseVersion != 0 {
bver = r.BaseVersion
}
if bver != 0 && r.BaseVersion == 0 {
r.BaseVersion = bver
}
if r.BaseVersion != bver {
return ErrVersionChange
}
if r.BaseName != "" {
bname = r.BaseName
}
if r.BaseSum != 0 {
bsum = r.BaseSum
}
name := bname + r.Name
if len(name) == 0 {
return ErrEmptyName
}
var valCnt int
if r.Value != nil {
valCnt++
}
if r.BoolValue != nil {
valCnt++
}
if r.DataValue != nil {
valCnt++
}
if r.StringValue != nil {
valCnt++
}
if valCnt > 1 {
return ErrTooManyValues
}
if r.Sum != nil || bsum != 0 {
valCnt++
}
if valCnt < 1 {
return ErrNoValues
}
if err := validateName(name); err != nil {
return err
}
}
return nil
}
func validateName(name string) error {
l := name[0]
if (l == '-') || (l == ':') || (l == '.') || (l == '/') || (l == '_') {
return ErrBadChar
}
//for _, l := range name {
// if (l < 'a' || l > 'z') && (l < 'A' || l > 'Z') && (l < '0' || l > '9') && (l != '-') && (l != ':') && (l != '.') && (l != '/') && (l != '_') {
// return ErrBadChar
// }
//}
return nil
}