-
Notifications
You must be signed in to change notification settings - Fork 0
/
OpenThings.py
870 lines (733 loc) · 23.4 KB
/
OpenThings.py
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
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
# OpenThings.py 27/09/2015 D.J.Whale
#
# Implement OpenThings message encoding and decoding
##from lifecycle import *
import time
try:
# Python 2
import crypto
except ImportError:
# Python 3
from . import crypto
def warning(msg):
print("warning:" + str(msg))
def trace(msg):
print("OpenThings:%s" % str(msg))
class OpenThingsException(Exception):
def __init__(self, value):
self.value = value
def __str__(self):
return repr(self.value)
#----- CRYPT PROCESSING -------------------------------------------------------
crypt_pid = None
def init(pid):
global crypt_pid
crypt_pid = pid
#----- PARAMETERS -------------------------------------------------------------
# report has bit 7 clear
# command has bit 7 set
PARAM_ALARM = 0x21
PARAM_DEBUG_OUTPUT = 0x2D
PARAM_IDENTIFY = 0x3F
PARAM_SOURCE_SELECTOR = 0x40 # command only
PARAM_WATER_DETECTOR = 0x41
PARAM_GLASS_BREAKAGE = 0x42
PARAM_CLOSURES = 0x43
PARAM_DOOR_BELL = 0x44
PARAM_ENERGY = 0x45
PARAM_FALL_SENSOR = 0x46
PARAM_GAS_VOLUME = 0x47
PARAM_AIR_PRESSURE = 0x48
PARAM_ILLUMINANCE = 0x49
PARAM_LEVEL = 0x4C
PARAM_RAINFALL = 0x4D
PARAM_APPARENT_POWER = 0x50
PARAM_POWER_FACTOR = 0x51
PARAM_REPORT_PERIOD = 0x52
PARAM_SMOKE_DETECTOR = 0x53
PARAM_TIME_AND_DATE = 0x54
PARAM_VIBRATION = 0x56
PARAM_WATER_VOLUME = 0x57
PARAM_WIND_SPEED = 0x58
PARAM_GAS_PRESSURE = 0x61
PARAM_BATTERY_LEVEL = 0x62
PARAM_CO_DETECTOR = 0x63
PARAM_DOOR_SENSOR = 0x64
PARAM_EMERGENCY = 0x65
PARAM_FREQUENCY = 0x66
PARAM_GAS_FLOW_RATE = 0x67
PARAM_RELATIVE_HUMIDITY=0x68
PARAM_CURRENT = 0x69
PARAM_JOIN = 0x6A
PARAM_LIGHT_LEVEL = 0x6C
PARAM_MOTION_DETECTOR = 0x6D
PARAM_OCCUPANCY = 0x6F
PARAM_REAL_POWER = 0x70
PARAM_REACTIVE_POWER = 0x71
PARAM_ROTATION_SPEED = 0x72
PARAM_SWITCH_STATE = 0x73
PARAM_TEMPERATURE = 0x74
PARAM_VOLTAGE = 0x76
PARAM_WATER_FLOW_RATE = 0x77
PARAM_WATER_PRESSURE = 0x78
PARAM_TEST = 0xAA
param_info = {
PARAM_ALARM : {"n":"ALARM", "u":""},
PARAM_DEBUG_OUTPUT : {"n":"DEBUG_OUTPUT", "u":""},
PARAM_IDENTIFY : {"n":"IDENTIFY", "u":""},
PARAM_SOURCE_SELECTOR : {"n":"SOURCE_SELECTOR", "u":""},
PARAM_WATER_DETECTOR : {"n":"WATER_DETECTOR", "u":""},
PARAM_GLASS_BREAKAGE : {"n":"GLASS_BREAKAGE", "u":""},
PARAM_CLOSURES : {"n":"CLOSURES", "u":""},
PARAM_DOOR_BELL : {"n":"DOOR_BELL", "u":""},
PARAM_ENERGY : {"n":"ENERGY", "u":"kWh"},
PARAM_FALL_SENSOR : {"n":"FALL_SENSOR", "u":""},
PARAM_GAS_VOLUME : {"n":"GAS_VOLUME", "u":"m3"},
PARAM_AIR_PRESSURE : {"n":"AIR_PRESSURE", "u":"mbar"},
PARAM_ILLUMINANCE : {"n":"ILLUMINANCE", "u":"Lux"},
PARAM_LEVEL : {"n":"LEVEL", "u":""},
PARAM_RAINFALL : {"n":"RAINFALL", "u":"mm"},
PARAM_APPARENT_POWER : {"n":"APPARENT_POWER", "u":"VA"},
PARAM_POWER_FACTOR : {"n":"POWER_FACTOR", "u":""},
PARAM_REPORT_PERIOD : {"n":"REPORT_PERIOD", "u":"s"},
PARAM_SMOKE_DETECTOR : {"n":"SMOKE_DETECTOR", "u":""},
PARAM_TIME_AND_DATE : {"n":"TIME_AND_DATE", "u":"s"},
PARAM_VIBRATION : {"n":"VIBRATION", "u":""},
PARAM_WATER_VOLUME : {"n":"WATER_VOLUME", "u":"l"},
PARAM_WIND_SPEED : {"n":"WIND_SPEED", "u":"m/s"},
PARAM_GAS_PRESSURE : {"n":"GAS_PRESSURE", "u":"Pa"},
PARAM_BATTERY_LEVEL : {"n":"BATTERY_LEVEL", "u":"V"},
PARAM_CO_DETECTOR : {"n":"CO_DETECTOR", "u":""},
PARAM_DOOR_SENSOR : {"n":"DOOR_SENSOR", "u":""},
PARAM_EMERGENCY : {"n":"EMERGENCY", "u":""},
PARAM_FREQUENCY : {"n":"FREQUENCY", "u":"Hz"},
PARAM_GAS_FLOW_RATE : {"n":"GAS_FLOW_RATE", "u":"m3/hr"},
PARAM_RELATIVE_HUMIDITY:{"n":"RELATIVE_HUMIDITY", "u":"%"},
PARAM_CURRENT : {"n":"CURRENT", "u":"A"},
PARAM_JOIN : {"n":"JOIN", "u":""},
PARAM_LIGHT_LEVEL : {"n":"LIGHT_LEVEL", "u":""},
PARAM_MOTION_DETECTOR : {"n":"MOTION_DETECTOR", "u":""},
PARAM_OCCUPANCY : {"n":"OCCUPANCY", "u":""},
PARAM_REAL_POWER : {"n":"REAL_POWER", "u":"W"},
PARAM_REACTIVE_POWER : {"n":"REACTIVE_POWER", "u":"VAR"},
PARAM_ROTATION_SPEED : {"n":"ROTATION_SPEED", "u":"RPM"},
PARAM_SWITCH_STATE : {"n":"SWITCH_STATE", "u":""},
PARAM_TEMPERATURE : {"n":"TEMPERATURE", "u":"C"},
PARAM_VOLTAGE : {"n":"VOLTAGE", "u":"V"},
PARAM_WATER_FLOW_RATE : {"n":"WATER_FLOW_RATE", "u":"l/hr"},
PARAM_WATER_PRESSURE : {"n":"WATER_PRESSURE", "u":"Pa"},
}
def paramname_to_paramid(paramname):
"""Turn a parameter name to a parameter id number"""
for paramid in param_info:
name = param_info[paramid]['n'] # name
if name == paramname:
return paramid
raise ValueError("Unknown param name %s" % paramname)
def paramid_to_paramname(paramid):
"""Turn a parameter id number into a parameter name"""
try:
return param_info[paramid]['n']
except KeyError:
return "UNKNOWN_%s" % str(hex(paramid))
#----- MESSAGE DECODER --------------------------------------------------------
#TODO: if silly lengths or silly types seen in decode, this might imply
#we're trying to process an encrypted packet without decrypting it.
#the code should be more robust to this (by checking the CRC)
def decode(payload, decrypt=True, receive_timestamp=None):
"""Decode a raw buffer into an OpenThings pydict"""
#Note, decrypt must already have run on this for it to work
length = payload[0]
# CHECK LENGTH
if length+1 != len(payload) or length < 10:
raise OpenThingsException("bad payload length")
##return {
## "type": "BADLEN",
## "len_actual": len(payload),
## "len_expected": length,
## "payload": payload[1:]
##}
# DECODE HEADER
mfrId = payload[1]
productId = payload[2]
encryptPIP = (payload[3]<<8) + payload[4]
header = {
"mfrid" : mfrId,
"productid" : productId,
"encryptPIP": encryptPIP
}
if decrypt:
# DECRYPT PAYLOAD
# [0]len,mfrid,productid,pipH,pipL,[5]
crypto.init(crypt_pid, encryptPIP)
crypto.cryptPayload(payload, 5, len(payload)-5) # including CRC
##printhex(payload)
# sensorId is in encrypted region
sensorId = (payload[5]<<16) + (payload[6]<<8) + payload[7]
header["sensorid"] = sensorId
# CHECK CRC
crc_actual = (payload[-2]<<8) + payload[-1]
crc_expected = calcCRC(payload, 5, len(payload)-(5+2))
##trace("crc actual:%s, expected:%s" %(hex(crc_actual), hex(crc_expected)))
if crc_actual != crc_expected:
raise OpenThingsException("bad CRC")
##return {
## "type": "BADCRC",
## "crc_actual": crc_actual,
## "crc_expected": crc_expected,
## "payload": payload[1:],
##}
# DECODE RECORDS
i = 8
recs = []
while i < length and payload[i] != 0:
# PARAM
param = payload[i]
wr = ((param & 0x80) == 0x80)
paramid = param & 0x7F
if paramid in param_info:
paramname = (param_info[paramid])["n"] # name
paramunit = (param_info[paramid])["u"] # unit
else:
paramname = "UNKNOWN_" + hex(paramid)
paramunit = "UNKNOWN_UNIT"
i += 1
# TYPE/LEN
typeid = payload[i] & 0xF0
plen = payload[i] & 0x0F
i += 1
rec = {
"wr": wr,
"paramid": paramid,
"paramname": paramname,
"paramunit": paramunit,
"typeid": typeid,
"length": plen
}
if plen != 0:
# VALUE
valuebytes = []
for x in range(plen):
valuebytes.append(payload[i])
i += 1
value = Value.decode(valuebytes, typeid, plen)
rec["valuebytes"] = valuebytes
rec["value"] = value
# store rec
recs.append(rec)
m = {
"type": "OK",
"header": header,
"recs": recs
}
if receive_timestamp != None:
m["rxtimestamp"] = receive_timestamp
return Message(m)
#----- MESSAGE ENCODER --------------------------------------------------------
#
# Encodes a message using the OpenThings message payload structure
# R1 message product id 0x02 monitor and control (in switching program?)
# C1 message product id 0x01 monitor only (in listening program)
def encode(spec, encrypt=True):
"""Encode a pydict specification into a OpenThings binary payload"""
# The message is not encrypted, but the CRC is generated here.
payload = []
# HEADER
payload.append(0) # length, fixup later when known
header = spec["header"]
payload.append(header["mfrid"])
payload.append(header["productid"])
if not ("encryptPIP" in header):
if encrypt:
warning("no encryptPIP in header, assuming 0x0100")
encryptPIP = 0x0100
else:
encryptPIP = header["encryptPIP"]
payload.append((encryptPIP&0xFF00)>>8) # MSB
payload.append((encryptPIP&0xFF)) # LSB
sensorId = header["sensorid"]
payload.append((sensorId>>16) & 0xFF) # HIGH
payload.append((sensorId>>8) & 0xFF) # MID
payload.append((sensorId) & 0XFF) # LOW
# RECORDS
for rec in spec["recs"]:
wr = rec["wr"]
paramid = rec["paramid"]
typeid = rec["typeid"]
if "length" in rec:
length = rec["length"]
else:
length = None # auto detect
# PARAMID
if wr:
payload.append(0x80 + paramid) # WRITE
else:
payload.append(paramid) # READ
# TYPE/LENGTH
payload.append((typeid)) # need to back patch length for auto detect
lenpos = len(payload)-1 # for backpatch
# VALUE
valueenc = [] # in case of no value
if "value" in rec:
value = rec["value"]
valueenc = Value.encode(value, typeid, length)
if len(valueenc) > 15:
raise ValueError("value longer than 15 bytes")
for b in valueenc:
payload.append(b)
payload[lenpos] = (typeid) | len(valueenc)
# FOOTER
payload.append(0) # NUL
crc = calcCRC(payload, 5, len(payload)-5)
payload.append((crc>>8) & 0xFF) # MSB
payload.append(crc&0xFF) # LSB
# back-patch the length byte so it is correct
payload[0] = len(payload)-1
if encrypt:
# ENCRYPT
# [0]len,mfrid,productid,pipH,pipL,[5]
crypto.init(crypt_pid, encryptPIP)
crypto.cryptPayload(payload, 5, len(payload)-5) # including CRC
return payload
#---- VALUE CODEC -------------------------------------------------------------
class Value():
UINT = 0x00
UINT_BP4 = 0x10
UINT_BP8 = 0x20
UINT_BP12 = 0x30
UINT_BP16 = 0x40
UINT_BP20 = 0x50
UINT_BP24 = 0x60
CHAR = 0x70
SINT = 0x80
SINT_BP8 = 0x90
SINT_BP16 = 0xA0
SINT_BP24 = 0xB0
# C0,D0,E0 RESERVED
FLOAT = 0xF0
@staticmethod
def typebits(typeid):
"""work out number of bits to represent this type"""
if typeid == Value.UINT_BP4: return 4
if typeid == Value.UINT_BP8: return 8
if typeid == Value.UINT_BP12: return 12
if typeid == Value.UINT_BP16: return 16
if typeid == Value.UINT_BP20: return 20
if typeid == Value.UINT_BP24: return 24
if typeid == Value.SINT_BP8: return 8
if typeid == Value.SINT_BP16: return 16
if typeid == Value.SINT_BP24: return 24
raise ValueError("Can't calculate number of bits for type:" + str(typeid))
@staticmethod
def highestClearBit(value, maxbits=15*8):
"""Find the highest clear bit scanning MSB to LSB"""
mask = 1<<(maxbits-1)
bitno = maxbits-1
while mask != 0:
##trace("compare %s with %s" %(hex(value), hex(mask)))
if (value & mask) == 0:
##trace("zero at bit %d" % bitno)
return bitno
mask >>= 1
bitno-=1
##trace("not found")
return None # NOT FOUND
@staticmethod
def valuebits(value):
"""Work out number of bits required to represent this value"""
if value >= 0 or type(value) != int:
raise RuntimeError("valuebits only on -ve int at moment")
if value == -1: # always 0xFF, so always needs exactly 2 bits to represent (sign and value)
return 2 # bits required
##trace("valuebits of:%d" % value)
# Turn into a 2's complement representation
MAXBYTES=15
MAXBITS = 1<<(MAXBYTES*8)
#TODO: check for truncation?
value = value & MAXBITS-1
##trace("hex:%s" % hex(value))
highz = Value.highestClearBit(value, MAXBYTES*8)
##trace("highz at bit:%d" % highz)
# allow for a sign bit, and bit numbering from zero
neededbits = highz+2
##trace("needed bits:%d" % neededbits)
return neededbits
@staticmethod
def encode(value, typeid, length=None):
##trace("encoding:" + str(value))
if typeid == Value.CHAR:
if type(value) != str:
value = str(value)
if length != None and len(str) > length:
raise ValueError("String too long")
result = []
for ch in value:
result.append(ord(ch))
if len != None and len(result) < length:
for a in range(length-len(result)):
result.append(0) # zero pad
return result
if typeid == Value.FLOAT:
raise ValueError("IEEE-FLOAT not yet supported")
if typeid <= Value.UINT_BP24:
# unsigned integer
if value < 0:
raise ValueError("Cannot encode negative number as an unsigned int")
if typeid != Value.UINT:
# pre-adjust for BP
if type(value) == float:
value *= (2**Value.typebits(typeid)) # shifts float into int range using BP
value = round(value, 0) # take off any unstorable bits
value = int(value) # It must be an integer for the next part of encoding
# code it in the minimum length bytes required
# Note that this codes zero in 0 bytes (might not be correct?)
v = value
result = []
while v != 0:
result.insert(0, v&0xFF) # MSB first, so reverse bytes as inserting
v >>= 8
# check length mismatch and zero left pad if required
if length != None:
if len(result) < length:
result = [0 for x in range(length-len(result))] + result
elif len(result) > length:
raise ValueError("Field width overflow, not enough bits")
return result
if typeid >= Value.SINT and typeid <= Value.SINT_BP24:
# signed int
if typeid != Value.SINT:
# pre-adjust for BP
if type(value) == float:
value *= (2**Value.typebits(typeid)) # shifts float into int range using BP
value = round(value, 0) # take off any unstorable bits
value = int(value) # It must be an integer for the next part of encoding
#If negative, take complement by masking with the length mask
# This turns -1 (8bit) into 0xFF, which is correct
# -1 (16bit) into 0xFFFF, which is correct
# -128(8bit) into 0x80, which is correct
#i.e. top bit will always be set as will all following bits up to number
if value < 0: # -ve
if typeid == Value.SINT:
bits = Value.valuebits(value)
else:
bits = Value.typebits(typeid)
##trace("need bits:" + str(bits))
# NORMALISE BITS TO BYTES
#round up to nearest number of 8 bits
# if already 8, leave 1,2,3,4,5,6,7,8 = 8 0,1,2,3,4,5,6,7 (((b-1)/8)+1)*8
# 9,10,11,12,13,14,15,16=16
bits = (((bits-1)/8)+1)*8 # snap to nearest byte boundary
##trace("snap bits to 8:" + str(bits))
value &= ((2**int(bits))-1)
neg = True
else:
neg = False
#encode in minimum bytes possible
v = value
result = []
while v != 0:
result.insert(0, v&0xFF) # MSB first, so reverse when inserting
v >>= 8
# if desired length mismatch, zero pad or sign extend to fit
if length != None: # fixed size
if len(result) < length: # pad
if not neg:
result = [0 for x in range(length-len(result))] + result
else: # negative
result = [0xFF for x in range(length-len(result))] + result
elif len(result) >length: # overflow
raise ValueError("Field width overflow, not enough bits")
return result
raise ValueError("Unknown typeid:%d" % typeid)
@staticmethod
def decode(valuebytes, typeid, length):
if typeid <= Value.UINT_BP24:
result = 0
# decode unsigned integer first
for i in range(length):
result <<= 8
result += valuebytes[i]
# process any fixed binary points
if typeid == Value.UINT:
return result # no BP adjustment
return (float(result)) / (2**Value.typebits(typeid))
elif typeid == Value.CHAR:
result = ""
for b in range(length):
result += chr(b)
return result
elif typeid >= Value.SINT and typeid <= Value.SINT_BP24:
# decode unsigned int first
result = 0
for i in range(length):
result <<= 8
result += valuebytes[i]
# turn to signed int based on high bit of MSB
# 2's comp is 1's comp plus 1
neg = ((valuebytes[0] & 0x80) == 0x80)
if neg:
onescomp = (~result) & ((2**(length*8))-1)
result = -(onescomp + 1)
# adjust for binary point
if typeid == Value.SINT:
return result # no BP, return as int
else:
# There is a BP, return as float
return (float(result))/(2**Value.typebits(typeid))
elif typeid == Value.FLOAT:
return "TODO_FLOAT_IEEE_754-2008" #TODO: IEEE 754-2008
raise ValueError("Unsupported typeid:%" + hex(typeid))
#----- CRC CALCULATION --------------------------------------------------------
def calcCRC(payload, start, length):
rem = 0
for b in payload[start:start+length]:
rem ^= (b<<8)
for bit in range(8):
if rem & (1<<15) != 0:
# bit is set
rem = ((rem<<1) ^ 0x1021) & 0xFFFF # always maintain U16
else:
# bit is clear
rem = (rem<<1) & 0xFFFF # always maintain U16
return rem
#----- MESSAGE UTILITIES ------------------------------------------------------
# SAMPLE MESSAGE
# SWITCH = {
# "header": {
# "mfrid": MFRID_ENERGENIE,
# "productid": PRODUCTID_MIHO005,
# "encryptPIP": CRYPT_PIP,
# "sensorid": 0 # FILL IN
# },
# "recs": [
# {
# "wr": True,
# "paramid": OpenThings.PARAM_SWITCH_STATE,
# "typeid": OpenThings.Value.UINT,
# "length": 1,
# "value": 0 # FILL IN
# }
# ]
# }
import copy
class Message():
BLANK = {
"header": {
"mfrid" : None,
"productid": None,
"sensorid": None
},
"recs":[]
}
def __init__(self, pydict=None, **kwargs):
if pydict == None:
pydict = copy.deepcopy(Message.BLANK)
self.pydict = pydict
self.set(**kwargs)
def __getitem__(self, key):
try:
# an integer key is used as a paramid in recs[]
key = int(key)
except:
# not an integer, so do a normal key lookup
# typically used for msg["header"] and msg["recs"]
# just returns a reference to that part of the inner pydict
return self.pydict[key]
# Is an integer, so index into recs[]
##print("looking up in recs")
for rec in self.pydict["recs"]:
if "paramid" in rec:
paramid = rec["paramid"]
if paramid == key:
return rec
raise KeyError("no paramid found for %s" % str(hex(key)))
def __setitem__(self, key, value):
"""set the header or the recs to the provided value"""
try:
key = int(key)
except:
# Not an parseable integer, so access by field name
##print("set by key")
self.pydict[key] = value
return
# Is an integer, so index into recs[]
##print("looking up in recs")
i = 0
for rec in self.pydict["recs"]:
if "paramid" in rec:
paramid = rec["paramid"]
if paramid == key:
##print("found at index %d %s" % (i, rec))
# add in the paramid
value["paramid"] = key
self.pydict["recs"][i] = value
return
i += 1
# Not found, so we should add it
print("no paramid for key %s, adding..." % str(hex(key)))
#TODO: add
# add in the paramid
value["paramid"] = key
self.pydict["recs"].append(value)
def copyof(self): # -> Message
"""Clone, to create a new message that is a completely independent copy"""
import copy
return Message(copy.deepcopy(self.pydict))
def set(self, **kwargs):
"""Set fields in the message from key value pairs"""
for key in kwargs:
value = kwargs[key]
# Is it a recs_PARAM_NAME_value format?
if key.startswith('recs_') and len(key)>6 and key[6].isupper():
self.set_PARAM_NAME(key[5:], value)
else:
# It's a full path
pathed_key = key.split('_')
m = self.pydict
# walk to the parent
for pkey in pathed_key[:-1]:
# If the key parseable as an integer, use as a list index instead
try:
pkey = int(pkey)
except:
pass
m = m[pkey]
# set the parent to have a key that points to the new value
pkey = pathed_key[-1]
# If the key parseable as an integer, use as a list index instead
try:
pkey = int(pkey)
except:
pass
# if index exists, change it, else create it
try:
m[pkey] = value
except IndexError:
# expand recs up to pkey
l = len(m) # length of list
gap = (l - pkey)+1
for i in range(gap):
m.append({})
m[pkey] = value
def set_PARAM_NAME(self, key, value):
"""Set a parameter given a PARAM_NAME key like recs_PARAM_NAME_field_nae"""
##print("set param name %s %s" % (key, value))
##key='recs_SWITCH_STATE'
#e.g. recs_SWITCH_STATE_value
#scan forward from char 5 until first lower case char, or end
pos = 0
last_uc=None
for c in key:
pos += 1
if c.isupper():
last_uc = pos
if c.islower(): break
name = key[:last_uc]
# turn PARAM_NAME into an integer id
param_id = paramname_to_paramid(name)
##print("paramid %d" % param_id)
# search for the id as a rec[]["paramid":v] value and get the rec
found = False
pos = 0
for rec in self.pydict["recs"]:
if "paramid" in rec:
if rec["paramid"] == param_id:
##print("found: %s" % rec)
found = True
break
pos += 1
if not found:
raise ValueError("No such paramid in message: %s" % name)
# is this rec_PARAM_NAME or rec_PARAM_NAME_field_name??
if len(key) == len(name):
##print("REC")
value["paramid"] = param_id
self.pydict["recs"][pos] = value
else:
##print("REC with field")
field_key = key[len(name)+1:]
self.pydict["recs"][pos][field_key] = value
def append_rec(self, *args, **kwargs):
"""Add a rec"""
if type(args[0]) == dict:
# This is ({})
self.pydict["recs"].append(args[0])
return len(self.pydict["recs"])-1 # index of rec just added
elif type(args[0]) == int:
if len(kwargs) == 0:
# This is (PARAM_x, pydict)
paramid = args[0]
pydict = args[1]
pydict["paramid"] = paramid
return self.append_rec(pydict)
else:
# This is (PARAM_x, key1=value1, key2=value2)
paramid = args[0]
# build a pydict
pydict = {"paramid":paramid}
for key in kwargs:
value = kwargs[key]
pydict[key] = value
self.append_rec(pydict)
else:
raise ValueError("Not sure how to parse arguments to append_rec")
def get(self, keypath):
"""READ(GET) from a single keypathed entry"""
path = keypath.split("_")
m = self.pydict
# walk to the final item
for pkey in path:
try:
pkey = int(pkey)
except:
pass
m = m[pkey]
return m
def __str__(self): # -> str
return str(self.pydict)
def dump(self):
msg = self.pydict
timestamp = None
# TIMESTAMP
if timestamp != None:
print("receive-time:%s" % time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(timestamp)))
# HEADER
if "header" in msg:
header = msg["header"]
def gethex(key):
if key in header:
value = header[key]
if value != None: return str(hex(value))
return ""
mfrid = gethex("mfrid")
productid = gethex("productid")
sensorid = gethex("sensorid")
print("mfrid:%s prodid:%s sensorid:%s" % (mfrid, productid, sensorid))
# RECORDS
if "recs" in msg:
for rec in msg["recs"]:
wr = rec["wr"]
if wr == True:
write = "write"
else:
write = "read "
try:
paramname = rec["paramname"] #NOTE: This only comes out from decoded messages
except:
paramname = ""
try:
paramid = rec["paramid"] #NOTE: This is only present on a input message (e.g SWITCH)
paramname = paramid_to_paramname(paramid)
paramid = str(hex(paramid))
except:
paramid = ""
try:
paramunit = rec["paramunit"] #NOTE: This only comes out from decoded messages
except:
paramunit = ""
if "value" in rec:
value = rec["value"]
else:
value = None
print("%s %s %s %s = %s" % (write, paramid, paramname, paramunit, str(value)))
# END