-
Notifications
You must be signed in to change notification settings - Fork 1
/
senml.go
476 lines (413 loc) · 14.2 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
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
// Package senml provides an implementation of RFC 8428
package senml
import (
"encoding/json"
"encoding/xml"
"fmt"
"regexp"
"sort"
"time"
)
// SupportedVersion declares the maximum version of the SenML format supported by this library
const SupportedVersion int = 10
// EncodingFormat declares the supported encoding formats of the SenML message
type EncodingFormat int
const (
// JSON will use encoding/json to serialize/deserialize the message
JSON EncodingFormat = iota
// XML will use encoding/xml to serialize/deserialize the message
XML
)
// Message is used to serialize and deserialize a SenML message
type Message struct {
/*
Used for XML parsing
*/
XMLName xml.Name `json:"-" xml:"urn:ietf:params:xml:ns:senml sensml"`
/*
Records of the message
*/
Records []Record `xml:"senml"`
}
// Record is a single record inside a SenML message
type Record struct {
/*
Used for XML parsing
*/
XMLName xml.Name `json:"-" xml:"senml"`
/*
This is a string that is prepended to the names found in the entries.
*/
BaseName *string `json:"bn,omitempty" xml:"bn,attr,omitempty"`
/*
A base time that is added to the time found in an entry.
*/
BaseTime *float64 `json:"bt,omitempty" xml:"bt,attr,omitempty"`
/*
A base unit that is assumed for all entries, unless
otherwise indicated. If a record does not contain a Unit value,
then the Base Unit is used. Otherwise, the value found in the
Unit (if any) is used.
*/
BaseUnit *string `json:"bu,omitempty" xml:"bu,attr,omitempty"`
/*
A base value is added to the value found in an entry, similar to Base Time.
*/
BaseValue *float64 `json:"bv:omitempty" xml:"bv,attr,omitempty"`
/*
A base sum is added to the sum found in an entry, similar to Base Time.
*/
BaseSum *float64 `json:"bs:omitempty" xml:"bs,attr,omitempty"`
/*
Version number of the media type format. This field is an optional positive integer and defaults to 10 if not present.
*/
BaseVersion *int `json:"bver,omitempty" xml:"bver,attr,omitempty"`
/*
Name of the sensor or parameter. When appended to the Base
Name field, this must result in a globally unique identifier for
the resource. The name is optional, if the Base Name is present.
If the name is missing, the Base Name must uniquely identify the
resource. This can be used to represent a large array of
measurements from the same sensor without having to repeat its
identifier on every measurement.
*/
Name *string `json:"n,omitempty" xml:"n,attr,omitempty"`
/*
Unit for a measurement value. Optional.
*/
Unit *string `json:"u,omitempty" xml:"u,attr,omitempty"`
/*
Value of the entry. Optional if a Sum value is present;
otherwise, it's required. Values are represented using basic data
types. This specification defines floating-point numbers ("v"
field for "Value"), booleans ("vb" for "Boolean Value"), strings
("vs" for "String Value"), and binary data ("vd" for "Data
Value"). Exactly one Value field MUST appear unless there is a
Sum field, in which case it is allowed to have no Value field.
*/
Value *float64 `json:"v,omitempty" xml:"v,attr,omitempty"`
BoolValue *bool `json:"vb,omitempty" xml:"vb,attr,omitempty"`
StringValue *string `json:"vs,omitempty" xml:"vs,attr,omitempty"`
DataValue *string `json:"vd,omitempty" xml:"vd,attr,omitempty"`
/*
Integrated sum of the values over time. Optional. This field
is in the unit specified in the Unit value multiplied by seconds.
For historical reasons, it is named "sum" instead of "integral".
*/
Sum *float64 `json:"s,omitempty" xml:"s,attr,omitempty"`
/*
Time when the value was recorded. Optional.
*/
Time *float64 `json:"t,omitempty" xml:"t,attr,omitempty"`
/*
Period of time in seconds that represents the maximum
time before this sensor will provide an updated reading for a
measurement. Optional. This can be used to detect the failure of
sensors or the communications path from the sensor.
*/
UpdateTime *float64 `json:"ut,omitempty" xml:"ut,attr,omitempty"`
}
// InvalidNameErrorReason declares the reason the name is invalid
type InvalidNameErrorReason int
const (
// FirstCharacterInvalid means that the first character of the resolved name is not allowed
FirstCharacterInvalid InvalidNameErrorReason = iota
// ContainsInvalidCharacter means that at least one not allowed character was used in the resolved name
ContainsInvalidCharacter
// Empty means that the resolved name is empty
Empty
)
// InvalidNameError is an error which is returned when the resolved name of a record is invalid. This can be due to it having an invalid first character, containing invalid characters or being empty. Please consult the RFC for more information.
type InvalidNameError struct {
// The reason why the resolved name is invalid
Reason InvalidNameErrorReason
}
func (err *InvalidNameError) Error() string {
switch err.Reason {
case FirstCharacterInvalid:
return "The resolved name is invalid. It MUST start with a character out of the set \"A\" to \"Z\", \"a\" to \"z\", or \"0\" to \"9\""
case ContainsInvalidCharacter:
return "The resolved name is invalid. It MUST consist only of characters out of the set \"A\" to \"Z\", \"a\" to \"z\", and \"0\" to \"9\", as well as \"-\", \":\", \".\", \"/\", and \"_\""
case Empty:
return "The resolved name is invalid. It MUST not be empty to uniquely identify and differentiate the sensor from all others"
default:
return "The resolved name is invalid. There is no detailed description for the given reason."
}
}
func newInvalidNameError(reason InvalidNameErrorReason) *InvalidNameError {
return &InvalidNameError{
Reason: reason,
}
}
// UnsupportedVersionError is an error which is returned when the message has an unsupported SenML version
type UnsupportedVersionError struct {
// The maximum version currently supported by this library
SupportedVersion int
// The version of the given message
GivenVersion int
}
func (err *UnsupportedVersionError) Error() string {
return fmt.Sprintf("The version of the message is unsupported. (maximum supported version: %v, got: %v)", err.SupportedVersion, err.GivenVersion)
}
func newUnsupportedVersionError(givenVersion int) *UnsupportedVersionError {
return &UnsupportedVersionError{
SupportedVersion: SupportedVersion,
GivenVersion: givenVersion,
}
}
// DifferentVersionError is an error which is returned when at least one record has a different BaseVersion than the others. This is not allowed since all records must have the same version number (RFC 8428 chapter 4.4).
type DifferentVersionError struct {
// The version currently used to parse the records. This could have been set by a preceding record or it defaults to the supported version if no BaseVersion field was set on a preceding record.
CurrentVersion int
// The version of the record which has a different version
GivenVersion int
}
func (err *DifferentVersionError) Error() string {
return fmt.Sprintf("The BaseVersion of at least one record differs from the other records. (version used to parse the records: %v, got: %v)", err.CurrentVersion, err.GivenVersion)
}
func newDifferentVersionError(currentVersion int, givenVersion int) *DifferentVersionError {
return &DifferentVersionError{
CurrentVersion: currentVersion,
GivenVersion: givenVersion,
}
}
// MissingValueError is an error which is returned when no value is set on the record. At least one of the following fields has to be set on all records: Value, StringValue, BoolValue, DataValue or Sum.
type MissingValueError struct {
}
func (err *MissingValueError) Error() string {
return "The record has no Value, StringValue, BoolValue, DataValue or Sum field set"
}
func newMissingValueError() *MissingValueError {
return &MissingValueError{}
}
// UnsupportedFormatError is an error which is returned when an unsupported encoding/decoding format was given.
type UnsupportedFormatError struct {
// The given format for encoding/decoding
GivenFormat EncodingFormat
}
func (err *UnsupportedFormatError) Error() string {
return fmt.Sprintf("Unsupported encoding/decoding format: %v", err.GivenFormat)
}
func newUnsupportedFormatError(format EncodingFormat) *UnsupportedFormatError {
return &UnsupportedFormatError{
GivenFormat: format,
}
}
// Decode parses the message with the given decoding format.
// Returns a non-resolved message, you need to resolve it using Resolve() to get
// base attributes resolution, absolute time, etc.
func Decode(encodedMessage []byte, format EncodingFormat) (message Message, err error) {
switch format {
case JSON:
err = json.Unmarshal(encodedMessage, &message.Records)
case XML:
err = xml.Unmarshal(encodedMessage, &message)
default:
err = newUnsupportedFormatError(format)
}
return
}
// Encode encodes the message with the given encoding format.
func (message Message) Encode(format EncodingFormat) ([]byte, error) {
switch format {
case JSON:
return json.Marshal(message.Records)
case XML:
return xml.Marshal(message)
default:
return nil, newUnsupportedFormatError(format)
}
}
// Resolve adds the base attributes to the normal attributes, calculates absolute time from relative time etc.
func (message Message) Resolve() (resolvedMessage Message, err error) {
var timeNow = float64(time.Now().Unix())
var baseName *string
var baseTime *float64
var baseUnit *string
var baseValue *float64
var baseSum *float64
var baseVersion *int
for _, record := range message.Records {
var resolvedRecord = Record{}
if record.BaseVersion != nil {
if *record.BaseVersion > SupportedVersion {
err = newUnsupportedVersionError(*record.BaseVersion)
return
} else if baseVersion == nil {
baseVersion = record.BaseVersion
} else if *record.BaseVersion != *baseVersion {
err = newDifferentVersionError(*baseVersion, *record.BaseVersion)
return
}
} else if baseVersion == nil {
var defaultVersion = SupportedVersion
baseVersion = &defaultVersion
}
if record.BaseName != nil {
baseName = record.BaseName
}
if record.BaseTime != nil {
baseTime = record.BaseTime
}
if record.BaseUnit != nil {
baseUnit = record.BaseUnit
}
if record.BaseValue != nil {
baseValue = record.BaseValue
}
if record.BaseSum != nil {
baseSum = record.BaseSum
}
var resolveNameError *InvalidNameError
resolvedRecord.Name, resolveNameError = resolveName(baseName, record.Name)
if resolveNameError != nil {
err = resolveNameError
return
}
resolvedRecord.Unit = resolveUnit(baseUnit, record.Unit)
resolvedRecord.Value = resolveValue(baseValue, record.Value)
resolvedRecord.BoolValue = resolveBoolValue(record.BoolValue)
resolvedRecord.StringValue = resolveStringValue(record.StringValue)
resolvedRecord.DataValue = resolveDataValue(record.DataValue)
resolvedRecord.Sum = resolveSum(baseSum, record.Sum)
resolvedRecord.Time = resolveTime(baseTime, record.Time, timeNow)
resolvedRecord.UpdateTime = resolveUpdateTime(record.UpdateTime)
var resolveValueError *MissingValueError
resolveValueError = validateRecordHasValue(resolvedRecord)
if resolveValueError != nil {
err = resolveValueError
return
}
resolvedMessage.Records = append(resolvedMessage.Records, resolvedRecord)
}
setBaseVersionIfNecessary(resolvedMessage, baseVersion)
sortRecordsChronologically(resolvedMessage.Records)
return
}
func resolveName(baseName *string, name *string) (*string, *InvalidNameError) {
var resolvedName string
if baseName != nil {
resolvedName = *baseName
}
if name != nil {
resolvedName += *name
}
if len(resolvedName) == 0 {
return nil, newInvalidNameError(Empty)
}
validFirstCharacterExp := regexp.MustCompile(`^[a-zA-Z0-9]*$`)
if !validFirstCharacterExp.MatchString(resolvedName[:1]) {
return nil, newInvalidNameError(FirstCharacterInvalid)
}
validNameCharsExp := regexp.MustCompile(`^[a-zA-Z0-9\-\:\.\/\_]*$`)
if !validNameCharsExp.MatchString(resolvedName) {
return nil, newInvalidNameError(ContainsInvalidCharacter)
}
return &resolvedName, nil
}
func resolveUnit(baseUnit *string, unit *string) *string {
if unit != nil {
var resolvedUnit = *unit
return &resolvedUnit
} else if baseUnit != nil {
var resolvedUnit = *baseUnit
return &resolvedUnit
}
return nil
}
func resolveValue(baseValue *float64, value *float64) *float64 {
var resolvedValue float64
if baseValue != nil {
resolvedValue = *baseValue
}
if value != nil {
resolvedValue += *value
}
if baseValue != nil || value != nil {
return &resolvedValue
}
return nil
}
func resolveBoolValue(value *bool) *bool {
if value != nil {
var resolvedBoolValue = *value
return &resolvedBoolValue
}
return nil
}
func resolveStringValue(value *string) *string {
if value != nil {
var resolvedStringValue = *value
return &resolvedStringValue
}
return nil
}
func resolveDataValue(value *string) *string {
if value != nil {
var resolvedDataValue = *value
return &resolvedDataValue
}
return nil
}
func resolveSum(baseSum *float64, sum *float64) *float64 {
var resolvedSum float64
if baseSum != nil {
resolvedSum = *baseSum
}
if sum != nil {
resolvedSum += *sum
}
if baseSum != nil || sum != nil {
return &resolvedSum
}
return nil
}
func resolveTime(baseTime *float64, time *float64, timeNow float64) *float64 {
var resolvedTime float64
if baseTime != nil {
resolvedTime = *baseTime
}
if time != nil {
resolvedTime += *time
}
if baseTime != nil || time != nil {
if resolvedTime < 2^28 {
resolvedTime += timeNow
}
return &resolvedTime
}
return nil
}
func resolveUpdateTime(updateTime *float64) *float64 {
if updateTime != nil {
var resolvedUpdateTime = *updateTime
return &resolvedUpdateTime
}
return nil
}
func validateRecordHasValue(record Record) *MissingValueError {
if record.Value == nil && record.StringValue == nil && record.BoolValue == nil && record.DataValue == nil && record.Sum == nil {
return newMissingValueError()
}
return nil
}
func setBaseVersionIfNecessary(message Message, baseVersion *int) {
if baseVersion != nil && *baseVersion < SupportedVersion {
for i := range message.Records {
var resolvedVersion = *baseVersion
message.Records[i].BaseVersion = &resolvedVersion
}
}
}
func sortRecordsChronologically(records []Record) {
sort.SliceStable(records, func(i, j int) bool {
var first, second = records[i], records[j]
if second.Time == nil {
return false
}
if first.Time == nil {
return true
}
return *first.Time < *second.Time
})
}