-
Notifications
You must be signed in to change notification settings - Fork 0
/
CreasolDomBusProtocol.py
1882 lines (1787 loc) · 110 KB
/
CreasolDomBusProtocol.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
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# Define constants and functions needed by CreasolDom RS485 bus protocol
"""
Protocol definition (in bytes):
========================= Protocol 1 ========================
0xda addr[2] length cmd|ACK|LEN arg1_1 [arg1_2] [arg1_3] cmd2 arg2_1 [arg2_2] [arg2_3] ...... checksum
preamble: 0x5a when transmitted by the master controller, 0xda when transmitted by the slave device
address (2 bytes):
0 => master (domoticz)
0xffff => broadcast
1 => default slave address
length: payload lenght (from cmd1 to checksum, excluding checksum)
cmd: syntax: CCCC ALLL where CCCC is the command family, A=1 when this is an acknowledge, LLL is the number of arguments for that command
args: at least 1 argument for each command
checksum: simply a checksum from preamble to checksum, excluding checksum
======================== Protocol 2 ======================== @20201216
0x3a dstAddr[2] srcAddr[2] length cmd|ACK|len/2 port parameters[1,3,5,7,9,11] cmd2|ACK|len2/2 port2 parameters2 cmd3|ACK|len3/2 port3 parameters3
"""
JSONURL = "http://127.0.0.1:8080/json.htm" #port may be different. Also, 127.0.0.1 should be enabled in the configuration "Trusted Network" field.
import Domoticz
import time
import struct
import re
import json
import math
import requests # needed to call a scene/group by a DCMD command from a module
#if 1, when a module does not transmit for more than 15 minutes (MODULE_ALIVE_TIME), it will appear in red (TimedOut)
PROTOCOL1_WITH_PERIODIC_TX=0 # set to 1 if all existing modules transmit their status periodically (oldest modules with protocol 1 did not)
#some constants
FRAME_LEN_MIN=7
FRAME_LEN_MIN2=9 #Min length of frame for protocol 2
FRAME_LEN_MAX=31 #max length for TX (devices cannot handle long frames)
FRAME_LEN=3
FRAME_LEN2=5
FRAME_HEADER=4
FRAME_HEADER2=6
PREAMBLE_MASTER=0x5a
PREAMBLE_DEVICE=0xda
PREAMBLE=0x3a #Preamble for protocol 2
CMD_LEN_MASK=0x07
CMD_MASK=0xf0
CMD_ACK=0x08
TX_RETRY=10 #max number of retries
TX_RETRY_TIME=80 # ms: retry every TX_RETRY_TIME * 2^retry
PERIODIC_STATUS_INTERVAL=300 #seconds: refresh output status to device every 5 minutes
MODULE_ALIVE_TIME=900 #if no frame is received in this time, module is considered dead (and periodic output status will not be transmitted)
CMD_CONFIG=0x00 #Config port
CMD_GET=0x10 #Get status
CMD_SET=0x20 #Set outputs/values
CMD_DCMD_CONFIG=0xe0 #Send DCMD configuration
CMD_DCMD=0xf0 #Receive DCMD command from Dombus
SUBCMD_CALIBRATE=0x00 #Send calibration value to a temperature/humidity sensor
SUBCMD_SET=0x01 #Send a 16bit value (used to change modbus device address and evseMaxCurrent)
SUBCMD_SET2=0x02 #Send parameter 2 (16bit value)
SUBCMD_SET3=0x03 #Send parameter 3 (16bit value)
SUBCMD_SET4=0x04 #Send parameter 4 (16bit value)
SUBCMD_SET5=0x05 #Send parameter 5 (16bit value)
SUBCMD_SET6=0x06 #Send parameter 6 (16bit value)
SUBCMD_SET7=0x07 #Send parameter 7 (16bit value)
SUBCMD_SET8=0x08 #Send parameter 8 (16bit value)
SUBCMD_SET9=0x09 #Send parameter 9 (16bit value)
SUBCMD_SET10=0x0a #Send parameter 10 (16bit value)
SUBCMD_SETMAX=0x10 #Send parameter 16
PORTTYPE_DISABLED=0x0000 #port not used
PORTTYPE_OUT_DIGITAL=0x0002 #digital, opto or relay output
PORTTYPE_OUT_RELAY_LP=0x0004 #relay output with lowpower PWM
PORTTYPE_OUT_LEDSTATUS=0x0008 #output used as led status
PORTTYPE_OUT_DIMMER=0x0010 #dimmer output, 0-100%
PORTTYPE_OUT_BUZZER=0x0020 #buzzer outputs (2 ports used as buzzer output, in push-pull)
PORTTYPE_OUT_FLASH=0x0020 #flash output, led, buzzer: same as OUT_BUZZER
PORTTYPE_IN_AC=0x0040 #input AC 50Hz (with optocoupler)
PORTTYPE_IN_DIGITAL=0x0080 #input digital
PORTTYPE_IN_ANALOG=0x0100 #input analog (ADC)
PORTTYPE_IN_TWINBUTTON=0x0200 #2 buttons connected to a single input through a resistor
PORTTYPE_IN_COUNTER=0x0400 #input pulses that increase a counter (incremental)
PORTTYPE_1WIRE=0x1000 #1 wire
PORTTYPE_SENSOR_DISTANCE=0x2000 #distance measurement (send a pulse and measure echo delay)
PORTTYPE_SENSOR_TEMP=0x4000 #Temperature
PORTTYPE_SENSOR_HUM=0x8000 #Relative Humidity
PORTTYPE_SENSOR_TEMP_HUM=0xc000 #Temp+Hum
PORTTYPE_OUT_BLIND=0x01000000 #Blind output, close command (next port of DomBus device will be automatically used as Blind output, open command)
PORTTYPE_OUT_ANALOG=0x02000000 #0-10V output, 1% step, 0-100
PORTTYPE_CUSTOM=0x80000000 #custom port with only 1 function
PORTOPT_NONE=0x0000 #No options
PORTOPT_INVERTED=0x0001 #Logical inverted: MUST BE 1
PORTOPT_PULLUP=0x0002 #pullup enabled
PORTOPT_PULLDOWN=0x0004 #pulldown enabled
#.....
#note: since version
PORTOPT_SELECTOR=0x0002 #Custom port configured as a selection switch to show/set different values
PORTOPT_DIMMER=0x0004 #Dimmer slide
PORTOPT_EV3PSELECT=0x00fe #Relay 2 of EVSE module used to enable 3phase
PORTOPT_ADDRESS=0x0100 #Modbus device address
PORTOPT_IMPORT_ENERGY=0x0102 #Total import energy in Wh*10 [32bit]
PORTOPT_EXPORT_ENERGY=0x0104 #Total export energy in Wh*10 [32bit]
PORTOPT_VOLTAGE=0x0106 #Voltage in Volt/10
PORTOPT_POWER_FACTOR=0x0108 #Power factore 1/1000
PORTOPT_FREQUENCY=0x010a #Frequency Hz/100
PORTOPT_CURRENT=0x010c
PORTTYPE={PORTTYPE_OUT_DIGITAL:244, PORTTYPE_OUT_RELAY_LP:244, PORTTYPE_OUT_LEDSTATUS:244, PORTTYPE_OUT_DIMMER:244, PORTTYPE_OUT_BUZZER:244, PORTTYPE_OUT_FLASH:244, PORTTYPE_IN_AC:244, PORTTYPE_IN_DIGITAL:244, PORTTYPE_IN_ANALOG:244, PORTTYPE_IN_TWINBUTTON:244, PORTTYPE_IN_COUNTER:243, PORTTYPE_SENSOR_HUM:81, PORTTYPE_SENSOR_TEMP:80, PORTTYPE_SENSOR_TEMP_HUM:82, PORTTYPE_SENSOR_DISTANCE:243, PORTTYPE_OUT_BLIND:244, PORTTYPE_OUT_ANALOG:244}
PORT_TYPENAME={PORTTYPE_OUT_DIGITAL:"Switch", PORTTYPE_OUT_RELAY_LP:"Switch", PORTTYPE_OUT_LEDSTATUS:"Switch", PORTTYPE_OUT_DIMMER:"Dimmer", PORTTYPE_OUT_BUZZER:"Selector Switch", PORTTYPE_OUT_FLASH:"Selector Switch", PORTTYPE_IN_AC:"Switch", PORTTYPE_IN_DIGITAL:"Switch", PORTTYPE_IN_ANALOG:"Voltage", PORTTYPE_IN_TWINBUTTON:"Selector Switch", PORTTYPE_IN_COUNTER:"Counter Incremental", PORTTYPE_SENSOR_HUM:"Humidity", PORTTYPE_SENSOR_TEMP:"Temperature", PORTTYPE_SENSOR_TEMP_HUM:"Temp+Hum", PORTTYPE_SENSOR_DISTANCE:"Distance", PORTTYPE_OUT_BLIND:"Switch", PORTTYPE_OUT_ANALOG:"Dimmer", PORTTYPE_CUSTOM:"Dimmer"}
PORTTYPES={
"DISABLED":0x0000, # port not used
"OUT_DIGITAL":0x0002, # relay output
"OUT_RELAY_LP":0x0004, # relay output
"OUT_LEDSTATUS":0x0008, # output used as led status
"OUT_DIMMER":0x0010, # dimmer output
"OUT_FLASH":0x0020, # buzzer output or flash or led flashing
"OUT_BUZZER":0x0020, # buzzer output (2 ports, push-pull)
"IN_AC":0x0040, # input AC 50Hz (with optocoupler)
"IN_DIGITAL":0x0080, # input digital
"IN_ANALOG":0x0100, # input analog (ADC)
"IN_TWINBUTTON":0x0200, # 2 buttons connected to a single input through a resistor
"IN_COUNTER":0x0400, # pulse counter
"DISTANCE":0x2000, # measure distance in mm
"TEMPERATURE":0x4000, # temperature
"HUMIDITY":0x8000, # relative humidity
"TEMP+HUM":0xc000, # temp+hum
"OUT_BLIND":0x01000000, # blind with up/down/stop command
"OUT_ANALOG":0x02000000, # 0-10V output, 0-100, 1% step
"CUSTOM":0x80000000, # Custom port (enabled only if PORTOPT is specified)
}
PORTOPTS={
"NORMAL":0x0000, # no options defined
"INVERTED":0x0001, # input or output is inverted (logic 1 means the corresponding GPIO is at GND
"PULLUP":0x0002,
"PULLDOWN":0x0004,
# options for CUSTOM device only
"SELECTOR":0x0002, # Selection switch
"DIMMER":0x0004, # Dimmer
"EV3PSELECT":0x00fe, # EVSE 3PSELECT
}
PORTTYPENAME={ #Used to set the device TypeName
"DISABLED":"Switch",
"OUT_DIGITAL":"Switch",
"OUT_RELAY_LP":"Switch",
"OUT_LEDSTATUS":"Switch", # output used as led status
"OUT_DIMMER":"Dimmer",
"OUT_BUZZER":"Selector Switch",
"OUT_FLASH":"Selector Switch",
"IN_AC":"Switch",
"IN_DIGITAL":"Switch",
"IN_ANALOG":"Voltage",
"IN_TWINBUTTON":"Selector Switch",
"IN_COUNTER":"Counter Incremental",
"HUMIDITY":"Humidity",
"TEMPERATURE":"Temperature",
"TEMP+HUM":"Temp+Hum",
"DISTANCE":"Distance",
# "OUT_BLIND":"Venetian Blinds EU", #not available in domoticz yet. hardware/plugins/PythonObjects.cpp must be updated!
"OUT_BLIND":"Switch",
"OUT_ANALOG":"Dimmer",
"CUSTOM":"Switch",
"SETPOINT":"Setpoint",
}
DCMD_IN_EVENTS={
"NONE": 0,
"OFF": 1,
"ON": 2,
"PULSE": 3, #short pulse
"PULSE1": 4, #1s pulse
"PULSE2": 5, #2s pulse
"PULSE4": 6, #4s pulse
"DIMMER": 7, #Dimming control
"VALUE": 8, #value (sensor, voltage, ...)
"ONUP": 9, #Twinbutton UP
"PULSEUP": 10,
"PULSEUP1": 11,
"PULSEUP2": 12,
"PULSEUP4": 13,
"PULSE3": 14,
"PULSEUP3": 15,
"MAX": 16, #max number of events
}
DCMD_OUT_CMDS={
"NONE": 0,
"OFF": 1, #Turn off output
"ON": 2, #turn ON output
"TOGGLE": 3, #toggle output ON -> OFF -> ON -> ....
"DIMMER": 4, #set value
"DOWN": 5, #Blind DOWN
"UP": 6, #Blind UP
"MAX": 7, #Max number of commands
}
TYPENAME2TYPESUB={
#Typename: [Type, SubType, Switchtype]
'Switch': [244,73,0],
'Dimmer': [244,73,7],
'Voltage': [243,8,0],
'Selector Switch': [244,62,0],
'Counter Incremental': [243,28,0],
'Humidity': [81,1,0],
'Temperature': [80,5,0],
'Temp+Hum': [82,1,0],
'Distance': [243,27,0],
}
rxbuffer=bytearray() #serial rx buffer
txbuffer=bytearray() #serial tx buffer
rxbufferindex=0
frameLen=0
frameAddr=0
checksumValue=0
devicesFull=False # True if no more space to store new devices
LASTRX=0 # first field in modules[]
LASTTX=1 # second field in modules[]
LASTSTATUS=2 # third field in modules[]
LASTPROTOCOL=3 # forth field in modules[]
LASTRETRY=4 # fifth field in modules[]: number of retries (used to compute the tx period)
modules={} # modules[frameAddr]=[lastRx, lastTx, lastSentStatus, protocol]
modulesAskConfig=[] #Dict with list of modules that require a new request of configuration (send CMD_CONFIG to ask for their configuration)
TXQ_CMD=0
TXQ_CMDLEN=1
TXQ_CMDACK=2
TXQ_PORT=3
TXQ_ARGS=4
TXQ_RETRIES=5
#txQueue[frameAddr].append([cmd,cmdLen,cmdAck,port,args,retries])
txQueue={} # tx queue for each module
LOG_NONE=0
LOG_ERR=1
LOG_WARN=2
LOG_INFO=3
LOG_DEBUG=4
LOG_DUMP=5
LOG_DUMPALL=6
LOG_NAMES=["NONE: ","ERROR:","WARN: ","INFO: ","DEBUG:","DUMP: "]
logLevel=LOG_NONE #defaul log level: will be initialized by onStart()
PORTSDISABLED="plugins/CreasolDomBus/portsDisabled.json"
counterTime={} #record the last counter update
counterOld={} #record the last value of counter (incremental value) to avoid decoding the same command twice, and avoid loosing of data
def __init__(self):
return
def portsDisabledWriteNow():
global portsDisabled
with open(PORTSDISABLED, 'w') as fd:
json.dump(portsDisabled,fd)
def portsDisabledInit():
global portsDisabled, portsDisabledWrite
portsDisabledWrite=0 #time*heartbeat before writing the portsDisabled dict on json file
try:
fd=open(PORTSDISABLED)
except:
# doesn't exist?
#Log(LOG_ERR,"Error opening file "+PORTDISABLED+": initializing portsDisabled...")
portsDisabled=dict()
portsDisabledWriteNow()
else:
portsDisabled=json.load(fd)
#portsDisabled={0xff23="3:4:5:11", 0xff31="7:8"} disabled ports, separated by colon. Port 1 can never be disabled!
def Log(level, msg):
global logLevel
if (level<=logLevel):
Domoticz.Log(LOG_NAMES[level]+msg)
return
def HRstatus(hum): #return normal, comfort, dry, wet status depending by relative humdity argument
if (hum<25):
return "2" #dry
elif (hum>70):
return "3" #wet
elif (hum>=40 and hum<=60): #comfortable
return "1"
else:
return "0" #normal
def checksum(protocol, buffer):
global checksumValue
checksumValue=0
if (protocol==1):
length=buffer[FRAME_LEN]+FRAME_HEADER
else:
length=buffer[FRAME_LEN2]+FRAME_HEADER2
for i in range(0, length):
checksumValue+=buffer[i]
checksumValue&=0xff
return
def dump(protocol, buffer,frameLen,direction):
#if protocol==0 => send to log with LOG_WARN priority. Used when a invalid frame is received
#buffer=frame buffer
#frameLen=length of frame in bytes
#direction="RX" or "TX"
if (logLevel<LOG_INFO): #in case of DCMD command, use Log(LOG_INFO,"") to transmit DCMD frame information
return
if protocol!=1 and int(buffer[0])==0x3a: # protocol 2 preamble
f="P:2 "
f+="%.2x " % int(buffer[0])
f+="%.4x " % (int(buffer[3])*256+int(buffer[4]))
f+="-> "
f+="%.4x " % (int(buffer[1])*256+int(buffer[2]))
f+="%.2d " % int(buffer[5]) #length
i=FRAME_HEADER2
while (i<frameLen-1):
#TODO: write SET 01 00 SET 02 01 SET 03 0a SET
#TODO: write CFG 02 FF
cmd=int(buffer[i])&CMD_MASK
cmdAck=int(buffer[i])&CMD_ACK
cmdLen=(int(buffer[i])&CMD_LEN_MASK)*2
if (cmdLen==0):
cmdLen=2 #minimum cmdLen
if (cmdAck):
f+='A-'
if (cmd==CMD_CONFIG):
f+='CFG '
if (cmdAck and (int(buffer[i+1])&0xf0)):
#whole port configuration => cmdLen without any sense
cmdLen=frameLen - FRAME_HEADER2 - 2 #force cmdLen to the whole frame
elif (cmd==CMD_SET):
f+='SET '
elif (cmd==CMD_GET):
f+='GET '
elif (cmd==CMD_DCMD_CONFIG):
f+='DCMDCFG '
elif (cmd==CMD_DCMD):
f+='DCMD '
else:
f+="%.2x " % int(buffer[i])
i+=1
if (i+cmdLen) > len(buffer):
Log(LOG_WARN, f"Frame error: cmdLen={cmdLen} is too much for the available data in buffer[]")
Log(LOG_WARN, f"Frame={f}...")
return
for j in range(0, cmdLen):
f+="%.2x " % int(buffer[i+j])
f+='| '
i+=cmdLen
f+="%.2x " % int(buffer[i]) #checksum
if (cmd==CMD_DCMD): #log DCMD command with priority INFO, so it's possible to monitor traffic between DomBus modules
Log(LOG_INFO,direction+" frame: "+f)
else:
Log(LOG_DUMP,direction+" frame: "+f)
else: # transmit data without parsing, as protocol=1
f="P:1 "
fl=frameLen if (frameLen<=len(buffer)) else len(buffer) #manage the case that frame is received only partially
for i in range(0, fl):
f+="%.2x " % int(buffer[i])
if protocol==0:
Log(LOG_WARN,direction+" frame: "+f)
else:
Log(LOG_DUMP,direction+" frame: "+f)
return
def getDeviceID(frameAddr, port):
global deviceID
global devID
global deviceAddr
deviceID="H{:04x}_P{:04x}".format(frameAddr, port)
devID="{:x}.{:x}".format(frameAddr, port)
deviceAddr="0x{:04x}".format(frameAddr)
return
def getDeviceUnit(Devices,findUnitFreeMax):
#given the deviceID (global var), scan Devices to find the Unit of the associated deviceID
#Also, set the variables
# UnitFree=greatest free unit, or lowest free unit
#
global deviceID, UnitFree
found=0xffff
unitmax=0
UnitFree=0xffff
for unit in range(1,256):
if (unit in Devices):
#Devices[unit] exists
if (Devices[unit].DeviceID==deviceID):
#this unit corresponds with the deviceID
found=unit
unitmax=unit+1
if (findUnitFreeMax==0):
#stop scanning
break
else:
#unit is free
if (UnitFree>unit):
UnitFree=unit
#Log(LOG_DEBUG,"getDeviceUnit(): found unit="+str(found)+" UnitFree="+str(UnitFree)+" UnitMax="+str(unitmax))
if (unitmax>0 and unitmax<256):
UnitFree=unitmax
return found
def getOpt(d, parameter):
#get parameter value from device description
#d=device
#parameter=STRING (must be uppercase)
#e.g. a=getOpt("A=")
#return a string:
# "false" if not defined
# "value" if A=value
# "1" if A= (needed when getOpt("INVERTED"), for example
opts=d.Description.upper().split(',')
length=len(parameter)
for opt in opts:
opt=opt.strip()
if (opt[:length]==parameter):
value=(opt[length:])
if (len(value)):
return value
else:
return "1"
return "false"
def convertValueToDombus(d, value):
#convert a value (20.5°C, 13.6V, ...) to dombus value (16bit integer)
#return the converted value
if (d.Type==PORTTYPE[PORTTYPE_SENSOR_TEMP]):
return int(value*10+2731) #temperature
elif (d.Type==PORTTYPE[PORTTYPE_SENSOR_HUM]):
return int(value*10) #Humidity
elif (d.Type==243 and (d.SubType==27 or d.SubType==8)): #distance or voltage
#extract A and B, if defined, to compute the right value VALUE=A*dombus_value+B => dombus_value=(VALUE-B)/A
v=getOpt(d,"A=")
a=float(v) if (v!="false") else 1
v=getOpt(d,"B=")
b=float(v) if (v!="false") else 0
return int((value-b)/a)
else:
return int(value)
def txQueueAdd(frameAddr, cmd,cmdLen,cmdAck,port,args,retries,now):
#add a command in the tx queue for the specified module (frameAddr)
#if that command already exists, update it
#cmdLen=length of data after command (port+args[])
global txQueue
sec=int(time.time())
ms=int(time.time()*1000)
protocol=2
if frameAddr in modules:
protocol=modules[frameAddr][LASTPROTOCOL]
if len(txQueue)==0 or frameAddr not in txQueue:
#create txQueue[frameAddr]
txQueue[frameAddr]=[[cmd, cmdLen, cmdAck, port, args, retries]]
#Log(LOG_DEBUG,"txQueueAdd (frameAddr does not exist) frameAddr="+hex(frameAddr)+" cmd="+hex(cmd|cmdAck|cmdLen)+" port="+hex(port))
else:
found=0
for f in txQueue[frameAddr]:
#f=[cmd,cmdlen,cmdAck,port,args[]]
if (f[TXQ_CMD]==cmd and f[TXQ_CMDLEN]==cmdLen and f[TXQ_PORT]==port and (cmd!=CMD_CONFIG or len(args)==0 or args[0]==f[TXQ_ARGS][0])): #if CMD_CONFIG, also check that SUBCMD is the same
#command already in txQueue: update values
f[TXQ_CMDACK]=cmdAck
f[TXQ_ARGS]=args
if (f[TXQ_RETRIES]<retries):
f[TXQ_RETRIES]=retries
found=1
break
if (found==0):
txQueue[frameAddr].append([cmd,cmdLen,cmdAck,port,args,retries])
#Log(LOG_DEBUG,"txQueueAdd (frame with same cmd,cmdLen... does not exist) frameAddr="+hex(frameAddr)+" cmd="+hex(cmd|cmdAck|cmdLen)+" port="+hex(port))
#txQueueRetry: don't modify it... transmit when retry time expires (maybe now or soon)
#check that modules[frameAddr] exists
#Log(LOG_DEBUG,"txQueueAdd: frameAddr="+hex(frameAddr)+" cmd="+hex(cmd|cmdAck|cmdLen)+" port="+hex(port))
if (frameAddr not in modules):
# add frameAddr in modules
# lastRx lastTx lastSentStatus => lastTx=0 => transmit now
modules[frameAddr]=[sec, ms, sec+3-PERIODIC_STATUS_INTERVAL, protocol, 0] #transmit output status in 3 seconds
else:
#frameAddr already in modules[]
if (now):
modules[frameAddr][LASTTX]=0 #transmit now
return
def txQueueAskConfig(protocol,frameAddr):
global devicesFull
if (devicesFull==0):
txQueueAdd(frameAddr,CMD_CONFIG,1,0,0xff,[],TX_RETRY,1) #port=0xff to ask full configuration
return
def txQueueRemove(frameAddr,cmd,port,arg1):
# if txQueue[frameAddr] esists, remove cmd and port from it.
# if cmd==255 and port==255 => remove all frames for module frameAddr
global txQueue
removeItems=[]
if len(txQueue)!=0 and frameAddr in txQueue:
for f in txQueue[frameAddr][:]:
#Log(LOG_DEBUG,"f="+str(f))
#f=[cmd,cmdlen,cmdAck,port,args[],retries]
if (((cmd&port)==255) or (f[TXQ_CMD]==cmd and f[TXQ_PORT]==port and (len(f[TXQ_ARGS])==0 or f[TXQ_ARGS][0]==arg1))):
txQueue[frameAddr].remove(f)
return
def txOutputsStatus(Devices,frameAddr):
# transmit the status of outputs for the device frameAddr
# Domoticz.Log("Send outputs status for device "+hex(frameAddr))
for Device in Devices:
deviceIDMask="H{:04x}_P".format(frameAddr)
d=Devices[Device]
if (d.Used==1 and d.DeviceID[:7]==deviceIDMask):
# device is used and matches frameAddr
# check that this is an output
if (d.Type==PORTTYPE[PORTTYPE_OUT_DIGITAL] and re.search('(OUT_DIGITAL|OUT_RELAY_LP|OUT_DIMMER|OUT_FLASH|OUT_BUZZER|OUT_ANALOG)',d.Description)):
# output! get the port and output state
port=int("0x"+d.DeviceID[7:11],0)
if (hasattr(d,'SwitchType') and d.SwitchType==7): #dimmer
if (re.search("OUT_ANALOG|CUSTOM.SELECTOR",d.Description)):
level=int(d.sValue) if d.nValue==1 else 0 #1% step
else:
level=int(int(d.sValue)/5) if d.nValue==1 else 0 #soft dimmer => 5% step
txQueueAdd(frameAddr,CMD_SET,2,0,port,[level],TX_RETRY,1) #Level: from 0 to 20 = 100%
elif (hasattr(d,'SwitchType') and d.SwitchType==18): #selector
txQueueAdd(frameAddr,CMD_SET,2,0,port,[d.nValue],TX_RETRY,1) #Level: 0, 10, 20, ....
else:
txQueueAdd(frameAddr,CMD_SET,2,0,port,[d.nValue],TX_RETRY,1)
return
def addOptions(Options, setOptions, newOptions):
# Options: original Options dict for a device
# setOptions: new Options dict
# newOptions: new options to add to setOptions
# return 1 if new options have changed/added
ret=0
for k,v in newOptions.items():
setOptions[k]=v
if (k not in Options) or Options[k]!=v:
ret+=1
return ret
def parseCommand(Devices, unit, Command, Level, Hue, frameAddr, port):
newstate=1 if Command=="On" else 0
#send command to the module
d=Devices[unit]
deviceID=d.DeviceID
#add command in the queue
Log(LOG_DEBUG,f"Type={d.Type} SubType={d.SubType} Name={d.Name} Command={Command} Level={Level} Hue={Hue}")
if (not hasattr(d,'SwitchType')):
Log(LOG_DEBUG,f"Type={d.Type} SubType={d.SubType} Name={d.Name} Command={Command} Level={Level} Hue={Hue}")
if (port&0xff00)==0x100: #SUBCMD_SET
Log(LOG_DEBUG,f"Virtual device that sends SUBCMD_SET command: port={port&0xff} Level={Level}")
txQueueAdd(frameAddr, CMD_CONFIG, 4, 0, (port&0xff) , [SUBCMD_SET, 0, int(Level)], TX_RETRY,0)
Log(LOG_DEBUG,f"Before Update(), setpoint name={d.Name} nValue={d.nValue} sValue={d.sValue}")
#d.Update(nValue=0, sValue=str(Level))
#Log(LOG_DEBUG,f"After Update(), setpoint name={d.Name} nValue={d.nValue} sValue={d.sValue}")
return
if (d.Type==PORTTYPE[PORTTYPE_OUT_DIGITAL]):
if (hasattr(d,'SwitchType') and d.SwitchType==7): #dimmer
if (Command=='Off'):
txQueueAdd(frameAddr,CMD_SET,2,0,port,[0],TX_RETRY,1) #Level: from 0 to 20 = 100%
else:
if (re.search('OUT_ANALOG|CUSTOM.DIMMER',d.Description)):
txQueueAdd(frameAddr,CMD_SET,2,0,port,[int(Level)],TX_RETRY,1) #Level: from 0 to 100 = 100% (1% step)
#Log(LOG_INFO,f"Dimmer level set to {Level} by parseCommand()")
else:
txQueueAdd(frameAddr,CMD_SET,2,0,port,[int(Level/5)],TX_RETRY,1) #Level: from 0 to 20 = 100% (5% step)
#nValue=0 if Level==0 else 1
#d.Update(nValue=1, sValue="20")
elif (hasattr(d,'SwitchType') and d.SwitchType==18): #selector
txQueueAdd(frameAddr,CMD_SET,2,0,port,[Level],TX_RETRY,1) #Level: 0, 10, 20, ....
elif (hasattr(d,'SwitchType') and (d.SwitchType==15 or d.SwitchType==14)): #venetian blinds
if (Command=='Off' or Command=='Open'): #Open
v=getOpt(d,"TIMEOPEN=")
duration=int(v) if (v!="false") else 25
if (duration<=0 or duration>120): duration=25
newstate=0x80|duration
elif (Command=='On' or Command=='Close'): #Close
v=getOpt(d,"TIMECLOSE=")
duration=int(v) if (v!="false") else 25
if (duration<=0 or duration>120): duration=25
newstate=duration
else: #Stop
newstate=0
#newstate=0 => STOP. newstate&0x80 => open for newstate&0x7f seconds, else close for newstate&0x7f seconds
txQueueAdd(frameAddr,CMD_SET,2,0,port,[newstate],TX_RETRY,1)
else: #normal switch
txQueueAdd(frameAddr,CMD_SET,2,0,port,[newstate],TX_RETRY,1)
elif d.Type==243:
if d.SubType==29: #kWh
#Transmit power value to DomBus, as signed16. Command=5410;0 (power;energy)
power=int(float(Command.split(';')[0]))
if (power<0): power=65536+power;
txQueueAdd(frameAddr,CMD_SET,4,0,port,[(power>>8), (power&0x0ff), 0], 1, 1)
else:
Log(LOG_DEBUG,"parseCommand: ignore command because Type="+str(d.Type)+" SubType="+str(d.SubType)+" Name="+d.Name+" has not attribute SwitchType")
return
def parseTypeOpt(Devices, Unit, opts, frameAddr, port):
#read device description and set the last defined type (if written in the description, else transmit 0xffff that mean NO-CHANGE) and the port options (ORed) (if no options are written in the description, transmits 0xffff that means NO CHANGE).
global txQueue, portsDisabled, portsDisabledWrite, deviceID, devID
getDeviceID(frameAddr,port)
Log(LOG_DEBUG,"Parse Description field for device "+devID)
setOpt=0
setType=0 #DomBus PORTTYPE value
setNewAddr='' #new address for modbus device
setHwaddr=0
setCal=32768 #calibration offset 32768=ignore
setTypeName=''
typeName=''
setOptDefined=0
setTypeDefined=0
setDisableDefined=0
setOptNames=""
setMaxCurrent=""
setMaxPower=""
setMaxPower2=""
setMaxPowerTime=""
setMaxPower2Time=""
setWaitTime=""
setMeterType=""
setStartPower=""
setStopTime=""
setAutoStart=""
Options=Devices[Unit].Options
setOptions={}
setOptionsChanged=0
#dombus command
# dcmd=[ # IN_EVENT, inValueL, inValueH, hwaddr, port, outCmd, outValue
# [ DCMD_IN_EVENTS['NONE'], 0, 0, 0, 0, DCMD_OUT_CMDS['NONE'], 0 ],
# ]
dcmd=[]
for opt in opts:
opt=opt.strip()
optu=opt.upper()
Log(LOG_DEBUG,"opt="+str(opt))
if optu in PORTTYPES:
#opt="OUT_DIGITAL" or DISTANCE or ....
setType=PORTTYPES[optu] # setType=0x2000 if DISTANCE is specified
setTypeDefined=1 #setTypeDefined=1
setTypeName=optu #setTypeName=DISTANCE
typeName=PORTTYPENAME[optu] #typeName=Temperature
Log(LOG_DEBUG,"opt="+str(opt)+" setType="+str(PORTTYPES[optu])+" typeName="+str(PORTTYPENAME[optu]))
elif optu in PORTOPTS:
if (optu=="NORMAL"):
setOpt=0
setOptNames=''
else:
setOpt=setOpt|PORTOPTS[optu]
setOptNames+=optu+","
setOptDefined=1
elif optu[:2]=="A=":
setOptNames+=opt+","
elif optu[:2]=="B=":
setOptNames+=opt+","
elif optu[:9]=="FUNCTION=":
# Used to convert an analog value to another
setOptionsChanged+=addOptions(Options, setOptions, {'function':optu[9:]})
setType=PORTTYPE_IN_ANALOG
setTypeDefined=1
typeName="Temperature"
setOptNames+=opt+","
elif optu[:4]=="CAL=": # calibration value: should be expressed as integer (e.g. temperatureOffset*10)
setCal=int(float(opt[4:])*10)
elif optu[:9]=="TYPENAME=":
typeName=opt[9:]
setOptNames+=opt+","
elif optu[:9]=="OPPOSITE=": #Used with kWh meter to set power to 0 when the opposite counter received a pulse (if import power >0, export power must be 0, and vice versa)
if (Devices[Unit].Type==243 and Devices[Unit].SubType==29): #kWh
opposite=int(float(opt[9:]))
if (Devices[opposite].Type==243 and Devices[opposite].SubType==29): #opposite unit is a kWh meter
Options['opposite']=str(opposite)
setOptNames+=opt+","
elif optu[:8]=="DIVIDER=": #Used with kWh meter to set how many pulses per kWh, e.g. 1000 (default), 2000, 1600, ...
if (Devices[Unit].Type==243 and Devices[Unit].SubType==29): #kWh
divider=int(float(opt[8:]))
if divider!=0:
Options['divider']=str(divider)
setOptNames+=opt+","
elif optu[:5]=="ADDR=":
addr=int(float(opt[5:]))
#Request command to change address of modbus device
if (addr!=None):
# Send command to program this device to the new address
if (addr>=1 and addr<=5):
setNewAddr=addr
elif optu[:13]=="EVMAXCURRENT=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setMaxCurrent=int(float(opt[13:]))
if (setMaxCurrent<6 or setMaxCurrent>36):
setMaxCurrent=16 # default value
setOptNames+=f"EVMAXCURRENT={setMaxCurrent},"
Log(LOG_INFO,f"setOptNames={setOptNames}")
elif optu[:11]=="EVMAXPOWER=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setMaxPower=int(float(opt[11:]))
if (setMaxPower<1000 or setMaxPower>25000):
setMaxPower=3300 # default value
setOptNames+=f"EVMAXPOWER={setMaxPower},"
elif optu[:12]=="EVMAXPOWER2=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setMaxPower2=int(float(opt[12:]))
if (setMaxPower2<1000 or setMaxPower2>25000):
setMaxPower2=0 # default value: ignore
setOptNames+=f"EVMAXPOWER2={setMaxPower2},"
elif optu[:15]=="EVMAXPOWERTIME=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setMaxPowerTime=int(float(opt[15:]))
if (setMaxPowerTime<60 or setMaxPowerTime>43200):
setMaxPowerTime=0 # default value: 0 => ignore
setOptNames+=f"EVMAXPOWERTIME={setMaxPowerTime},"
elif optu[:16]=="EVMAXPOWER2TIME=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setMaxPower2Time=int(float(opt[16:]))
if (setMaxPower2Time<60 or setMaxPower2Time>43200):
setMaxPower2Time=0 # default value: 0 => ignore
setOptNames+=f"EVMAXPOWER2TIME={setMaxPower2Time},"
elif optu[:13]=="EVSTARTPOWER=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setStartPower=int(float(opt[13:]))
if (setStartPower<800 or setStartPower>25000):
setStartPower=1200 # default value
setOptNames+=f"EVSTARTPOWER={setStartPower},"
elif optu[:11]=="EVSTOPTIME=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setStopTime=int(float(opt[11:]))
if (setStopTime<5 or setStopTime>600):
setStopTime=90 # default value
setOptNames+=f"EVSTOPTIME={setStopTime},"
elif optu[:12]=="EVAUTOSTART=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setAutoStart=int(float(opt[12:]))
if (setAutoStart>1):
setAutoStart=1 # default value
setOptNames+=f"EVAUTOSTART={setAutoStart},"
elif optu[:11]=="EVWAITTIME=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setWaitTime=int(float(opt[11:]))
if (setWaitTime==0):
setWaitTime=6 # default value
setOptNames+=f"EVWAITTIME={setWaitTime},"
elif optu[:12]=="EVMETERTYPE=" and ("EV Mode" in Devices[Unit].Name or "EV State" in Devices[Unit].Name):
setMeterType=int(float(opt[12:]))
if (setMeterType>1):
setMeterType=0 # default value
setOptNames+=f"EVMETERTYPE={setMeterType},"
elif optu[:9]=="HWADDR=0X" and len(opt)==13:
#set hardware address
hwaddr=int(optu[7:],16)
if (hwaddr>=1 and hwaddr<65535):
setHwaddr=hwaddr
elif optu[:8]=="DISABLE=":
#set which ports are enabled, for this device, and remove disabled ports to free space in Devices[]
#syntax: DISABLE=1:2:3:4:5:10:11
setOptNames+="DISABLE="
portsDisabledOld=[]
if deviceAddr in portsDisabled:
portsDisabledOld=portsDisabled[deviceAddr] # save old number of disabled ports
portsDisabled[deviceAddr]=[]
portsDisabledWrite=3 #write portDisabled on a json file in 3*heartbeat_interval (3*10s)
setDisableDefined=1
modulesAskConfig.append(frameAddr)
for ps in optu[8:].split(':'):
try:
p=int(ps)
except:
#skip this
Log(LOG_WARN,"DISABLE= wrong port "+ps)
else:
if (p>1 and p<=32):
if p not in portsDisabled[deviceAddr]:
portsDisabled[deviceAddr].append(p)
if p in portsDisabledOld:
portsDisabledOld.remove(p)
#remove the device corresponding to frameAddr and disabled port
deviceid="H{:04x}_P{:04x}".format(frameAddr, p)
for u in Devices:
if Devices[u].DeviceID==deviceid:
Log(LOG_DEBUG,"Removing device "+deviceid+"...")
Devices[u].Delete()
#TODO
break
setOptNames+=str(p)+":"
if (setOptNames[-1:]==':'):
setOptNames=setOptNames[:-1] #remove trailing ':'
if not portsDisabledOld:
# in the old DISABLE= there were ports that now are enabled => request port configuration to enable previously disabled ports
txQueueAskConfig(protocol, frameAddr)
setOptNames+=','
elif (optu[:6]=="DESCR=" or optu[:10]=="TIMECLOSE=" or optu[:9]=="TIMEOPEN="):
setOptNames+=opt+","
elif (optu[:5]=="DCMD("):
#command to another dombus
errmsg=''
d=[ DCMD_IN_EVENTS['NONE'], 0, 0, 0, 0, DCMD_OUT_CMDS['NONE'], 0 ] #temp list to store a DCMD command
opt=re.sub("ERROR=.*", "", opt) #remove any Error=blablabla from the command
optu=opt.upper()
inputs=re.search('DCMD\((.+)\)=(.+\..+:.+)', optu)
if inputs:
#syntax of DCMD command semms to be ok
inArr=inputs.group(1).split(':') #inArr=['Value','0','20.5'] (inputs)
outArr=inputs.group(2).split(':') #
if (len(inArr)>=1):
Log(LOG_INFO,"DCMD: "+opt+" Input event="+str(inArr)+" Output command="+str(outArr))
if (inArr[0] in DCMD_IN_EVENTS):
d[0]=DCMD_IN_EVENTS[inArr[0]]
d[1]=0
d1ok=0
d[2]=0
d2ok=0
if (len(inArr)>=2):
# inArr[1] contains a temperature, humidity, voltage,... convert this value to a integer representation used by DomBus
try:
d[1]=float(inArr[1])
except:
errmsg+="ValueLow should be a number, like 20.5. "
d[1]=0
else:
d1ok=1
try:
d[2]=float(inArr[2])
except:
errmsg+="ValueHigh should be a number, like 21.2. "
d[2]=0
else:
d2ok=1
if (inArr[0]=='VALUE'):
#convert d[1] and d[2] in temperature, RH, voltage, value according to the sensor type and A and B parameters
if (d1ok):
d[1]=convertValueToDombus(Devices[Unit],d[1])
if (d2ok):
d[2]=convertValueToDombus(Devices[Unit],d[2])
Log(LOG_DEBUG,"d[1]="+str(d[1])+" d[2]="+str(d[2]))
if (len(outArr)>=2):
if (outArr[1] in DCMD_OUT_CMDS):
#outArr[0]=101.4
#outArr[1]=ON
hwaddrport=outArr[0].split('.')
#TODO: ALL.BLIND
d[3]=int(hwaddrport[0],16)
d[4]=int(hwaddrport[1],16)
d[5]=int(DCMD_OUT_CMDS[outArr[1]])
d[6]=0 #outValue
if (len(outArr)>=3):
#outArr[2]=30m
# From 0 to 60s => 31.25ms resolution 0=0, 1920=60s
# From 1m to 1h with 1s resolution 1921=61s, 3540+1920=5460=1h
# From 1h to 1d with 1m resolution 5461=1h+1m, 1380+5460=6840=24h
# From 1d to forever with 1h resolution 6841=25h
if (outArr[2].isnumeric()):
#value * 31.5ms
d[6]=int(outArr[2])
else:
outValue=(outArr[2][:-1])
outUM=outArr[2][-1:]
#value in seconds
if (outValue.isnumeric()):
outValue=int(outValue)
if (outUM=='S'): #seconds
if (outValue<=60):
d[6]=outValue*32
elif (outValue<=3600):
d[6]=1920+(outValue-60)
elif (outUM=='M'): #minutes
if (outValue<=1):
d[6]=outValue*60*32
elif (outValue<=60):
d[6]=1920+(outValue-1)*60
elif (outValue<=1440):
d[6]=5460+(outValue-60)
elif (outUM=='H'): #hours
if (outValue<=1):
d[6]=outValue*5460
elif(outValue<=24):
d[6]=5460+(outValue-1)*60
else:
d[6]=6840+(outValue-24)
elif (outUM=='D'):
d[6]=6840+(outValue-1)*24
if (d[6]>65535):
d[6]=1826*24+6840 #max 5 years = 1826 days
errmsg+='Max time = 1826 days;'
dcmd.append(d) #add record to dcmd[]
else:
errmsg="Command not recognized;"
Log(LOG_WARN,"DCMD: Command "+outArr[1]+" not recognized, possible commands="+str(list(DCMD_OUT_CMDS)))
Log(LOG_DEBUG,"DCMD: "+opt)
else:
errmsg="At least HWADDR.PORT:COMMAND expected after =;"
Log(LOG_WARN,"DCMD: Address.Port:Command : invalid syntax. Address.Port="+outArr[0]+" ,Command="+outArr[1])
Log(LOG_DEBUG,"DCMD: "+opt)
else:
errmsg="Event not recognized inside ();"
Log(LOG_WARN,"DCMD: Event not recognized: event="+inArr[0]+" , possible events="+str(list(DCMD_IN_EVENTS.keys())))
Log(LOG_DEBUG,"DCMD: "+opt)
else:
errmsg="At least 1 parameter expected inside ();"
Log(LOG_WARN,"DCMD: no parameters specified inside ()")
Log(LOG_DEBUG,"DCMD: "+opt)
else:
errmsg="Invalid syntax;"
Log(LOG_WARN,"DCMD: invalid syntax")
Log(LOG_DEBUG,"DCMD: "+opt)
if (len(errmsg)>0):
opt+=':Error='+errmsg+': Valid command is like DCMD(Value:0:20.5)=101.1:ON:30m'
#reset values inside the current DCMD array
setOptNames+="\n"+opt+"," #DCMD OK:
if (setOptNames!=''):
setOptNames=setOptNames[:-1] #remove last comma ,
Log(LOG_INFO,"Config device "+hex(frameAddr)+": type="+hex(setType)+" typeName="+typeName+" Options="+str(Options))
if (setHwaddr!=0 and setHwaddr!=0xffff): #hwaddr not 0
# remove HWADDR=0X1234 option from the device description
# send command to change hwaddr
Log(LOG_DEBUG,f"Set HWADDR={setHwaddr}")
txQueueAdd(frameAddr, CMD_CONFIG, 4, 0, 0, [(setHwaddr >> 8), (setHwaddr&0xff), (0-(setHwaddr >> 8)-(setHwaddr&0xff)-0xa5)], TX_RETRY,1)
elif (setNewAddr!=''): # set modbus address
# send command to change modbus addr
Log(LOG_INFO,f"Send command to change modbus device address to {setNewAddr}")
txQueueAdd(frameAddr, CMD_CONFIG, 4, 0, port, [SUBCMD_SET, (setNewAddr>>8), (setNewAddr&0xff)], TX_RETRY, 1) #EVSE: until 2023-04-24 port must be replaced with port+5 to permit changing modbus address
else:
if frameAddr in modules and modules[frameAddr][LASTPROTOCOL]==1:
txQueueAdd(frameAddr, CMD_CONFIG, 5, 0, port, [((setType>>8)&0xff), (setType&0xff), (setOpt >> 8), (setOpt&0xff)], TX_RETRY,0) #PORTTYPE_VERSION=1
else:
txQueueAdd(frameAddr, CMD_CONFIG, 7, 0, port, [((setType>>24)&0xff), ((setType>>16)&0xff), ((setType>>8)&0xff), (setType&0xff), (setOpt >> 8), (setOpt&0xff)], TX_RETRY,0) #PORTTYPE_VERSION=2
if modules[frameAddr][LASTPROTOCOL] != 1:
#Transmit Dombus CMD config
dcmdnum=0
for i in range(0,min(len(dcmd),8)):
d=dcmd[i]
#note: port|=0, 0x20, 0x40, 0x60 (4 DCMD for each port)
if (d[0]!=0 and d[0]<DCMD_IN_EVENTS["MAX"]):
dcmdnum+=1
txQueueAdd(frameAddr,CMD_DCMD_CONFIG, 12, 0, port|(i<<5), [ d[0],
d[1]>>8, d[1]&0xff,
d[2]>>8, d[2]&0xff,
d[3]>>8, d[3]&0xff, d[4], d[5],
d[6]>>8, d[6]&0xff ], TX_RETRY, 0)
if (dcmdnum==0): #DCMD not defined => transmits an empty DCMD_CONFIG
txQueueAdd(frameAddr, CMD_DCMD_CONFIG, 2, 0, port, [ DCMD_IN_EVENTS["NONE"] ], TX_RETRY, 0)
else:
#protocol==1 does not support DCMD_CONFIG command (too long)
Log(LOG_WARN,f"Device {devID} does not support protocol #2 and DCMD commands: frameAddr={frameAddr} protocol={modules[frameAddr][LASTPROTOCOL]}")
descr='ID='+devID+','+setTypeName+','+setOptNames if (setTypeDefined==1) else setOptNames
# Now checking Options , removing bad ones
if typeName in TYPENAME2TYPESUB and Devices[Unit].Type!=TYPENAME2TYPESUB[typeName][0] and Devices[Unit].SubType!=TYPENAME2TYPESUB[typeName][1]:
# different typename from before
nValue=0
sValue='0'
if typeName!='Selector Switch':
if 'LevelNames' in Options: del Options['LevelNames']
if 'LevelOffHidden' in Options: del Options['LevelOffHidden']
if 'SelectorStyle' in Options: del Options['SelectorStyle']
else:
sValue='0'
if typeName!='Counter Incremental' and typeName!='kWh':
if 'divider' in Options: del Options['divider']
if 'EnergyMeterMode' in Options: del Options['EnergyMeterMode']
if 'SignedWatt' in Options: del Options['SignedWatt']
if 'opposite' in Options: del Options['opposite']
else:
sValue='0;0'
if typeName!='Custom':
if 'Custom' in Options: del Options['Custom']
if typeName!='Temperature':
if 'function' in Options: del Options['function']
if 'avgTemp' in Options: del Options['avgTemp']
else:
sValue="0"
else:
#same TypeName as before => device not changed
nValue=Devices[Unit].nValue
sValue=Devices[Unit].sValue
if (setTypeName=="IN_TWINBUTTON" or setTypeName=="OUT_BLIND"): #selector
if "LevelNames" not in Options:
Options["LevelNames"]="Off|Down|Up"
#Options["LevelOffHidden"]="false"
#Options["SelectorStyle"]="0"
Options.update(setOptions)
# if (setOptionsChanged>0):
Log(LOG_INFO,"TypeName='"+str(typeName)+"', nValue="+str(nValue)+", sValue='"+str(sValue)+"', Description='"+str(descr)+"', Options="+str(Options))
Devices[Unit].Update(TypeName=typeName, nValue=nValue, sValue=sValue, Description=str(descr), Options=Options) # Update description (removing HWADDR=0x1234)
if (setCal!=32768): #new calibration value
if (setCal<0):
setCal+=65536
txQueueAdd(frameAddr, CMD_CONFIG, 4, 0, port, [SUBCMD_CALIBRATE, ((setCal>>8)&0xff), (setCal&0xff)], TX_RETRY, 0)
if (setMaxCurrent!=""): # EV Mode: set max current (cable)
txQueueAdd(frameAddr, CMD_CONFIG, 4, 0, port, [SUBCMD_SET, 0, setMaxCurrent], TX_RETRY,0)
if (setMaxPower!=""): # EV Mode: set max power from grid (contractual power + 10%)
txQueueAdd(frameAddr, CMD_CONFIG, 4, 0, port, [SUBCMD_SET2, setMaxPower>>8, setMaxPower&0xff], TX_RETRY,0)