· 6 years ago · Nov 07, 2019, 03:36 PM
1# deCONZ Bridge
2#
3# Author: Smanar
4#
5"""
6<plugin key="deCONZ" name="deCONZ plugin" author="Smanar" version="1.0.10" wikilink="https://github.com/Smanar/Domoticz-deCONZ" externallink="https://www.dresden-elektronik.de/funktechnik/products/software/pc-software/deconz/?L=1">
7 <description>
8 <br/><br/>
9 <h2>deCONZ Bridge</h2><br/>
10 It use the deCONZ rest api to make a bridge beetween your zigbee network and Domoticz (Using Conbee or Raspbee)
11 <br/><br/>
12 <h3>Remark</h3>
13 <ul style="list-style-type:square">
14 <li>You can use the file API_KEY.py if you have problems to get your API Key or your Websocket Port</li>
15 <li>You can find updated files for deCONZ on their github : https://github.com/dresden-elektronik/deconz-rest-plugin</li>
16 <li>If you want the plugin works without connexion, use as IP 127.0.0.1 (if deCONZ and domoticz are on same machine)</li>
17 <li>If you are running the plugin for the first time, better to enable debug log (Take Debug info Only)</li>
18 </ul>
19 <h3>Supported Devices</h3>
20 <ul style="list-style-type:square">
21 <li>https://github.com/dresden-elektronik/deconz-rest-plugin/wiki/Supported-Devices</li>
22 </ul>
23 <h3>Configuration</h3>
24 Gateway configuration
25 </description>
26 <params>
27 <param field="Address" label="deCONZ IP" width="150px" required="true" default="127.0.0.1"/>
28 <param field="Port" label="Port" width="150px" required="true" default="80"/>
29 <param field="Mode2" label="API KEY" width="75px" required="true" default="1234567890" />
30 <param field="Mode3" label="Debug" width="150px">
31 <options>
32 <option label="None" value="0" default="true" />
33 <option label="Debug info Only" value="2"/>
34 <option label="Basic Debugging" value="62"/>
35 <option label="Basic+Messages" value="126"/>
36 <option label="Connections Only" value="16"/>
37 <option label="Connections+Python" value="18"/>
38 <option label="Connections+Queue" value="144"/>
39 <option label="All" value="-1"/>
40 </options>
41 </param>
42 </params>
43</plugin>
44"""
45
46# All imports
47import Domoticz
48
49import json,urllib, time
50
51REQUESTPRESENT = True
52try:
53 import requests
54except:
55 REQUESTPRESENT = False
56
57from fonctions import rgb_to_xy, rgb_to_hsl, xy_to_rgb
58from fonctions import Count_Type, ProcessAllState, ProcessAllConfig, First_Json, JSON_Repair, get_JSON_payload
59from fonctions import ButtonconvertionXCUBE, ButtonconvertionXCUBE_R, ButtonconvertionTradfriRemote, ButtonconvertionTradfriSwitch, ButtonconvertionGeneric, VibrationSensorConvertion
60
61#from requests import async
62
63#Better to use 'localhost' ?
64DOMOTICZ_IP = '127.0.0.1'
65
66LIGHTLOG = True #To disable some activation, log will be lighter, but less informations.
67SETTODEFAULT = False #To set device in default state after a rejoin
68
69#https://github.com/febalci/DomoticzEarthquake/blob/master/plugin.py
70#https://stackoverflow.com/questions/32436864/raw-post-request-with-json-in-body
71
72class BasePlugin:
73
74 #enabled = False
75
76 def __init__(self):
77 self.Devices = {} # id, type, state (banned/missing/working) , model
78 self.NeedToReset = []
79 self.Ready = False
80 self.Buffer_Command = []
81 self.Buffer_Time = ''
82 self.WebSocket = None
83 self.WebsoketBuffer = ''
84 self.Banned_Devices = []
85 self.BufferReceive = ''
86 self.BufferLenght = 0
87
88 self.INIT_STEP = ['config','lights','sensors','groups']
89
90 return
91
92 def onStart(self):
93 Domoticz.Debug("onStart called")
94 #CreateDevice('1111','sensors','On/Off light')
95
96 #Check Domoticz IP
97 if Parameters["Address"] != '127.0.0.1' and Parameters["Address"] != 'localhost':
98 global DOMOTICZ_IP
99 DOMOTICZ_IP = get_ip()
100 Domoticz.Log("Your haven't use 127.0.0.1 as IP, so I suppose deCONZ and Domoticz aren't on same machine")
101 Domoticz.Log("Taking " + DOMOTICZ_IP + " as Domoticz IP")
102
103 if DOMOTICZ_IP == Parameters["Address"]:
104 Domoticz.Status("Your have same IP for deCONZ and Domoticz why don't use 127.0.0.1 as IP")
105 else:
106 Domoticz.Log("Domoticz and deCONZ are on same machine")
107
108 if Parameters["Mode3"] != "0":
109 Domoticz.Debugging(int(Parameters["Mode3"]))
110 #DumpConfigToLog()
111
112 #Read banned devices
113 with open(Parameters["HomeFolder"]+"banned_devices.txt", 'r') as myPluginConfFile:
114 for line in myPluginConfFile:
115 if not line.startswith('#'):
116 self.Banned_Devices.append(line.strip())
117 myPluginConfFile.close()
118
119 #Read and Set config
120 #json = '{"websocketnotifyall":true}'
121 #url = '/api/' + Parameters["Mode2"] + '/config/'
122 #self.SendCommand(url,json)
123
124 # Disabled, not working for selector ...
125 #check for new icons
126 #if 'bulbs_group' not in Images:
127 # try:
128 # Domoticz.Image('icons/bulbs_group.zip').Create()
129 # except:
130 # Domoticz.Error("Can't create new icons")
131
132 def onStop(self):
133 Domoticz.Debug("onStop called")
134 if self.WebSocket:
135 self.WebSocket.Disconnect()
136
137 def onConnect(self, Connection, Status, Description):
138 Domoticz.Debug("onConnect called")
139
140 if Connection.Name == 'deCONZ_WebSocket':
141
142 if (Status != 0):
143 Domoticz.Error("WebSocket connexion error : " + str(Connection))
144 Domoticz.Error("Status : " + str(Status) + " Description : " + str(Description) )
145 return
146
147 Domoticz.Status("Launching websocket on port " + str(Connection.Port) )
148 #Need to Add Sec-Websocket-Protocol : domoticz ????
149 #Boring error > Socket Shutdown Error: 9, Bad file descriptor
150 wsHeader = "GET / HTTP/1.1\r\n" \
151 "Host: "+ Parameters["Address"] + ':' + str(Connection.Port) + "\r\n" \
152 "User-Agent: Domoticz/1.0\r\n" \
153 "Sec-WebSocket-Version: 13\r\n" \
154 "Origin: http://" + DOMOTICZ_IP + "\r\n" \
155 "Sec-WebSocket-Key: qqMLBxyyjz9Tog1bll7K6A==\r\n" \
156 "Connection: keep-alive, Upgrade\r\n" \
157 "Upgrade: websocket\r\n\r\n"
158 #"Accept: Content-Type: text/html; charset=UTF-8\r\n" \
159 #"Pragma: no-cache\r\n" \
160 #"Cache-Control: no-cache\r\n" \
161 #"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n" \
162 self.WebSocket.Send(wsHeader)
163
164 else:
165 Domoticz.Error("Unknow connexion : " + str(Connection))
166 Domoticz.Error("Status : " + str(Status) + " Description : " + str(Description) )
167 return
168
169 def onMessage(self, Connection, Data):
170 Domoticz.Debug("onMessage called")
171
172 _Data = []
173
174 if self.WebsoketBuffer:
175 Data = self.WebsoketBuffer + Data
176 self.WebsoketBuffer = ''
177
178 #Domoticz.Log("Data : " + str(Data))
179 #Domoticz.Log("Connexion : " + str(Connection))
180 #Domoticz.Log("Byte needed : " + str(Connection.BytesTransferred()) + "ATM : " + str(len(Data)))
181 #The max is 4096 so if the data size excess 4096 byte it will be cut
182
183 #Websocket data ?
184 if (Connection.Name == 'deCONZ_WebSocket'):
185 #Data = b'\x81W{"e":"changed","id":"7","r":"groups","state":{"all_on":true,"any_on":true}}'
186 #Data = b'\x81W{"e":"changed","id":"5","r":"groups","state":{"all_on":true,"any_on"'
187
188 if Data.startswith(b'\x81'):
189 while len(Data) > 0:
190 try:
191 payload, extra_data = get_JSON_payload(Data)
192 except:
193 if (Data[0:1] == b'\x81') and (len(str(Data)) < 300) :
194 self.WebsoketBuffer = Data
195 Domoticz.Log("Incomplete Json keep it for later : " + str(self.WebsoketBuffer) )
196 else:
197 Domoticz.Error("Malformed JSON response, can't repair : " + str(Data) )
198 break
199 _Data.append(payload)
200 Data = extra_data
201
202 for js in _Data:
203 self.WebSocketConnexion(js)
204 else:
205 Domoticz.Log("Websocket Handshake : " + str(Data.decode("utf-8", "ignore").replace('\n','***')) )
206
207 else:
208 Domoticz.Log("Unknow Connection" + str(Connection))
209 Domoticz.Log("Data : " + str(Data))
210 return
211
212 def onCommand(self, Unit, Command, Level, Hue):
213 Domoticz.Log("onCommand called for Unit " + str(Unit) + ": Parameter '" + str(Command) + "', Level: " + str(Level) + ", Hue: " + str(Hue))
214
215 if not self.Ready == True:
216 Domoticz.Error("deCONZ not ready")
217 return
218
219 _type,deCONZ_ID = self.GetDevicedeCONZ(Devices[Unit].DeviceID)
220
221 if not deCONZ_ID:
222 Domoticz.Error("Device not ready : " + str(Unit) )
223 return
224
225 if _type == 'sensors':
226 Domoticz.Error("This device don't support action")
227 return
228
229 _json = {}
230
231 #on/off
232 if Command == 'On':
233 _json['on'] = True
234 if Level:
235 _json['bri'] = round(Level*254/100)
236 if Command == 'Off':
237 _json['on'] = False
238
239 #level
240 if Command == 'Set Level':
241 #To prevent bug
242 _json['on'] = True
243
244 _json['bri'] = round(Level*254/100)
245
246 #thermostat situation
247 if _type == 'config':
248 _json.clear()
249 _json['mode'] = "auto"
250 _json['heatsetpoint'] = Level
251
252 #color
253 if Command == 'Set Color':
254
255 #To prevent bug
256 _json['on'] = True
257
258 Hue_List = json.loads(Hue)
259
260 #ColorModeNone = 0 // Illegal
261 #ColorModeNone = 1 // White. Valid fields: none
262 if Hue_List['m'] == 1:
263 ww = int(Hue_List['ww']) # Can be used as level for monochrome white
264 #TODO : Jamais vu un device avec ca encore
265 Domoticz.Debug("Not implemented device color 1")
266 #ColorModeTemp = 2 // White with color temperature. Valid fields: t
267 if Hue_List['m'] == 2:
268 #Value is in mireds (not kelvin)
269 #Correct values are from 153 (6500K) up to 588 (1700K)
270 # t is 0 > 255
271 TempKelvin = int(((255 - int(Hue_List['t']))*(6500-1700)/255)+1700);
272 TempMired = 1000000 // TempKelvin
273 #if previous not working
274 #TempMired = round(float(Hue_List['t'])*(500.0f - 153.0f) / 255.0f + 153.0f)
275 _json['ct'] = TempMired
276 #ColorModeRGB = 3 // Color. Valid fields: r, g, b.
277 elif Hue_List['m'] == 3:
278 IEEE = Devices[Unit].DeviceID
279 if self.Devices[IEEE].get('colormode','Unknow') == 'hs':
280 h,l,s = rgb_to_hsl((int(Hue_List['r']),int(Hue_List['g']),int(Hue_List['b'])))
281 hue = int(h * 65535)
282 saturation = int(s * 254)
283 value = int(l * 254/100)
284 _json['hue'] = hue
285 _json['sat'] = saturation
286 _json['bri'] = value
287 _json['transitiontime'] = 0
288 else:
289 x, y = rgb_to_xy((int(Hue_List['r']),int(Hue_List['g']),int(Hue_List['b'])))
290 x = round(x,6)
291 y = round(y,6)
292 _json['xy'] = [x,y]
293 #ColorModeCustom = 4, // Custom (color + white). Valid fields: r, g, b, cw, ww, depending on device capabilities
294 elif Hue_List['m'] == 4:
295 ww = int(Hue_List['ww'])
296 cw = int(Hue_List['cw'])
297 x, y = rgb_to_xy((int(Hue_List['r']),int(Hue_List['g']),int(Hue_List['b'])))
298 #TODO, Pas trouve de device avec ca encore ...
299 Domoticz.Debug("Not implemented device color 2")
300
301 #To prevent bug
302 if '"bri":' not in _json:
303 _json['bri'] = round(Level*254/100)
304 _json['transitiontime'] = 0
305
306
307 url = '/api/' + Parameters["Mode2"] + '/' + _type + '/' + str(deCONZ_ID)
308 if _type == 'lights':
309 url = url + '/state'
310 elif _type == 'config':
311 url = '/api/' + Parameters["Mode2"] + '/sensors/' + str(deCONZ_ID) + '/config'
312 elif _type == 'scenes':
313 url = '/api/' + Parameters["Mode2"] + '/groups/' + deCONZ_ID.split('/')[0] + '/scenes/' + deCONZ_ID.split('/')[1] + '/recall'
314 _json = {} # to force PUT
315 else:
316 url = url + '/action'
317
318 self.SendCommand(url,_json)
319
320 def onNotification(self, Name, Subject, Text, Status, Priority, Sound, ImageFile):
321 Domoticz.Log("Notification: " + Name + "," + Subject + "," + Text + "," + Status + "," + str(Priority) + "," + Sound + "," + ImageFile)
322
323 def onDisconnect(self, Connection):
324 Domoticz.Status("onDisconnect called for " + str(Connection.Name) )
325
326 def onHeartbeat(self):
327 Domoticz.Debug("onHeartbeat called")
328
329 #Check for freeze
330 if len(self.Buffer_Command) > 0:
331 self.UpdateBuffer()
332
333 #Initialisation
334 if self.Ready != True:
335 if len(self.INIT_STEP) > 0:
336 Domoticz.Debug("### Initialisation > " + str(self.INIT_STEP[0]))
337 self.ManageInit()
338
339 #Stop all here
340 return
341 else:
342 self.Ready = True
343
344 #Check websocket connexion
345 if self.WebSocket:
346 if not self.WebSocket.Connected():
347 Domoticz.Error("WebSocket Disconnected, reconnexion !")
348 self.WebSocket.Connect()
349
350 #reset switchs
351 if len(self.NeedToReset) > 0 :
352 for IEEE in self.NeedToReset:
353 _id = False
354 for i in self.Devices:
355 if i == IEEE:
356 _id = self.Devices[i]['id']
357 UpdateDevice(_id,'sensors', { 'nValue' : 0 , 'sValue' : 'Off' } )
358 self.NeedToReset = []
359
360 #Devices[27].Update(nValue=0, sValue='11;22' )
361
362 def onDeviceRemoved(self,unit):
363 Domoticz.Log("Device Removed")
364 #TODO : Need to rescan all
365
366#---------------------------------------------------------------------------------------
367
368 def ManageInit(self,pop = False):
369
370 if pop:
371 self.INIT_STEP.pop(0)
372 if len(self.INIT_STEP) < 1:
373 self.Ready = True
374
375 Domoticz.Status("### deCONZ ready")
376 l,s,g,b,o,c = Count_Type(self.Devices)
377 Domoticz.Status("### Found " + str(l) + " Operators, " + str(s) + " Sensors, " + str(g) + " Groups, " + str(c) + " Scenes and " + str(o) + " others, with " + str(b) + " Ignored")
378
379 # Compare devices bases
380 for i in Devices:
381 if Devices[i].DeviceID not in self.Devices:
382 Domoticz.Status('### Device ' + Devices[i].DeviceID + '(' + Devices[i].Name + ') Not in deCONZ ATM, the device is deleted or not ready.')
383
384 return
385
386 #No flood during initialisation
387 if len(self.Buffer_Command) > 0:
388 u,d = self.Buffer_Command[-1]
389 if "/" + self.INIT_STEP[0] + "/" in u:
390 Domoticz.Log("### Still waiting")
391 return
392
393 Domoticz.Log("### Request " + self.INIT_STEP[0])
394 self.SendCommand("/api/" + Parameters["Mode2"] + "/" + self.INIT_STEP[0] + "/")
395
396 def InitDomoticzDB(self,key,_Data,Type_device):
397
398 return
399 #Lights or sensors ?
400 if 'uniqueid' in _Data:
401
402 IEEE = str(_Data['uniqueid'])
403 Name = str(_Data['name'])
404 Type = str(_Data['type'])
405 Model = str(_Data.get('modelid',''))
406 if not Model:
407 Model = ''
408
409 #Type_device = 'lights'
410 #if not 'hascolor' in _Data[i]:
411 # Type_device = 'sensors'
412
413 Domoticz.Log("### Device > " + str(key) + ' Name:' + Name + ' Type:' + Type + ' Details:' + str(_Data['state']))
414
415 self.Devices[IEEE] = {'id' : key , 'type' : Type_device , 'model' : Type , 'state' : 'working'}
416
417 #Skip banned device
418 if IEEE in self.Banned_Devices:
419 Domoticz.Log("Skipping Device (Banned) : " + str(IEEE) )
420 self.Devices[IEEE]['state'] = 'banned'
421 return
422
423 #Get some infos
424 kwarg = {}
425 if 'state' in _Data:
426 state = _Data['state']
427 kwarg.update(ProcessAllState(state,Model))
428 if 'colormode' in state:
429 self.Devices[IEEE]['colormode'] = state['colormode']
430
431 if 'config' in _Data:
432 config = _Data['config']
433 kwarg.update(ProcessAllConfig(config))
434
435 #It's a switch ? Need special process
436 if Type == 'ZHASwitch' or Type == 'ZGPSwitch' or Type == 'CLIPSwitch':
437
438 #Set it to off
439 kwarg.update({'sValue': 'Off', 'nValue': 0})
440
441 #ignore ZHASwitch if vibration sensor
442 if 'sensitivity' in _Data['config']:
443 return
444
445 if 'lumi.sensor_cube' in Model:
446 if IEEE.endswith('-03-000c'):
447 Type = 'XCube_R'
448 elif IEEE.endswith('-02-0012'):
449 Type = 'XCube_C'
450 else:
451 # Useless device
452 self.Devices[IEEE]['state'] = 'banned'
453 return
454 elif 'TRADFRI remote control' in Model:
455 Type = 'Tradfri_remote'
456 #elif 'RWL021' in Model:
457 # Type = 'Tradfri_remote'
458 elif 'TRADFRI on/off switch' in Model:
459 Type = 'Tradfri_on/off_switch'
460 else:
461 Type = 'Switch_Generic'
462
463 self.Devices[IEEE]['model'] = Type
464
465 if self.Ready == True:
466 Domoticz.Status("Adding missing device :" + str(key) + ' Type:' + str(Type))
467
468 #Not exist > create
469 if GetDomoDeviceInfo(IEEE) == False:
470 #Special devices
471 if Type == 'ZHAThermostat':
472 #Create a setpoint device
473 self.Devices[IEEE + "_heatsetpoint"] = {'id' : key , 'type' : 'config' , 'state' : 'working' , 'model' : 'ZHAThermostat' }
474 CreateDevice(IEEE + "_heatsetpoint" ,Name,'ZHAThermostat')
475 #Transform the current device in tmeperature device
476 Type = 'ZHATemperature'
477
478 CreateDevice(IEEE,Name,Type)
479
480 #update
481 if kwarg:
482 UpdateDevice(key,Type_device,kwarg)
483
484 #groups
485 else:
486
487 Name = str(_Data['name'])
488 Type = str(_Data['type'])
489 Domoticz.Log("### Groupe > " + str(key) + ' Name:' + Name )
490 Dev_name = 'GROUP_' + Name.replace(' ','_')
491 self.Devices[Dev_name] = {'id' : key , 'type' : 'groups' , 'model' : 'groups', 'state' : 'working'}
492
493 # Skip banned group
494 if Dev_name in self.Banned_Devices:
495 Domoticz.Log("Skipping Group (Banned) : " + str(Dev_name) )
496 self.Devices[Dev_name]['state'] = 'banned'
497
498 else:
499 #Check for scene
500 scenes = _Data.get('scenes',[])
501 if len(scenes) > 0:
502 for j in scenes:
503 Domoticz.Log("### Scenes associated with group " + str(key) + " > ID:" + str(j['id']) + " Name:" + str(j['name']) )
504 Scene_name = 'SCENE_' + str(j['name']).replace(' ','_')
505 self.Devices[Scene_name] = {'id' : str(key) + '/' + str(j['id']) , 'type' : 'scenes' , 'model' : 'scenes'}
506 #^scene not exist > create
507 if GetDomoDeviceInfo(Scene_name) == False:
508 CreateDevice(Scene_name,str(j['name']),'Scenes')
509
510 #Group not exist > create
511 if GetDomoDeviceInfo(Dev_name) == False:
512 CreateDevice(Dev_name,Name,Type)
513
514
515 def NormalConnexion(self,_Data):
516
517 Domoticz.Debug("Classic Data : " + str(_Data) )
518
519 #JSON with data returned >> _Data = [{'success': {'/lights/2/state/on': True}}, {'success': {'/lights/2/state/bri': 251}}]
520 if isinstance(_Data, list):
521 self.ReadReturn(_Data)
522 else:
523 if (self.Ready != True):
524 #JSON with config
525 if 'bridgeid' in _Data:
526 if 'websocketnotifyall' in _Data:
527 self.ReadConfig(_Data)
528 else:
529 Domoticz.Error("Bad API KEY !")
530 else:
531 #JSON with device info like {'1': {'data:1}}
532 for i in _Data:
533 self.InitDomoticzDB(i,_Data[i],self.INIT_STEP[0])
534
535 #Update initialisation
536 self.ManageInit(True)
537 else:
538 #JSON with device info like {'data:1}
539 typ,_id = self.GetDevicedeCONZ(_Data.get('uniqueid','') )
540 if _id:
541 self.InitDomoticzDB(_id,_Data,typ)
542 #Check for groups
543 else:
544 _id = _Data.get('id','')
545 if _id:
546 self.InitDomoticzDB(_id,_Data,'groups')
547
548 def ReadReturn(self,_Data):
549 kwarg = {}
550 _id = False
551 _type = False
552
553 for _Data2 in _Data:
554
555 First_item = next(iter(_Data2))
556
557 #Command Error
558 if First_item == 'error':
559 Domoticz.Error("deCONZ error :" + str(_Data2))
560 if _Data2['error']['type'] == 3:
561 dev = _Data2['error']['address'].split('/')
562 _id = dev[2]
563 _type = dev[1]
564 #Set red header
565 kwarg.update({'TimedOut':1})
566
567 #Command sucess
568 elif First_item == 'success':
569
570 data = _Data2['success']
571 dev = (list(data.keys())[0] ).split('/')
572 val = data[list(data.keys())[0]]
573
574 if len(dev) < 3:
575 pass
576 else:
577 if not _id:
578 _id = dev[2]
579 _type = dev[1]
580
581 if dev[1] == 'config':
582 Domoticz.Status("Editing configuration : " + str(data) )
583
584 else:
585 Domoticz.Error("Not managed return JSON : " + str(_Data2) )
586
587 if kwarg:
588 UpdateDevice(_id,_type,kwarg)
589
590 def ReadConfig(self,_Data):
591 #trick to test is deconz is ready
592 fw = _Data['fwversion']
593 if fw == '0x00000000':
594 Domoticz.Error("Wrong startup, retrying !!")
595 #Cancel this part to restart it after 1 heartbeat (10s)
596 return
597 Domoticz.Status("Firmware version : " + _Data['fwversion'] )
598 Domoticz.Status("Websocketnotifyall : " + str(_Data['websocketnotifyall']))
599 if not _Data['websocketnotifyall'] == True:
600 Domoticz.Error("Websocketnotifyall is not set to True")
601
602 #Launch Web socket connexion
603 self.WebSocket = Domoticz.Connection(Name="deCONZ_WebSocket", Transport="TCP/IP", Address=Parameters["Address"], Port=str(_Data['websocketport']) )
604 self.WebSocket.Connect()
605
606 self.ManageInit(True)
607
608 def WebSocketConnexion(self,_Data):
609
610 Domoticz.Debug("### WebSocket Data : " + str(_Data) )
611
612 if not self.Ready == True:
613 Domoticz.Error("deCONZ not ready")
614 return
615
616 if 'e' in _Data:
617 if _Data['e'] == 'deleted':
618 return
619 if _Data['e'] == 'added':
620 return
621 if _Data['e'] == 'scene-called':
622 Domoticz.Log("Playing scene > group:" + str(_Data['gid']) + " Scene:" + str(_Data['scid']) )
623 return
624
625 #Take care, no uniqueid for groups
626 IEEE,state = self.GetDeviceIEEE(_Data['id'],_Data['r'])
627
628 #Patch for device with double UniqueID
629 if (not IEEE) and ('uniqueid' in _Data):
630 typ,_id = self.GetDevicedeCONZ(_Data['uniqueid'] )
631 if _id:
632 Domoticz.Log("Double UniqueID correction : " + _Data['id'] + ' > ' + str(_id) )
633 _Data['id'] = _id
634 IEEE,state = self.GetDeviceIEEE(_Data['id'],_Data['r'])
635
636 if not IEEE:
637 if 'uniqueid' in _Data:
638
639 Domoticz.Error("Websocket error, unknow device > " + str(_Data['id']) + ' (' + str(_Data['r']) + ') Asking for information')
640 IEEE = str(_Data['uniqueid'])
641 #Try getting informations
642 self.Devices[IEEE] = {'id' : str(_Data['id']) , 'type' : str(_Data['r']) , 'state' : 'missing'}
643 self.SendCommand('/api/' + Parameters["Mode2"] + '/' + str(_Data['r']) + '/' + str(_Data['id']) )
644 else:
645 Domoticz.Error("Websocket error, unknow device > " + str(_Data['id']) + ' (' + str(_Data['r']) + ')')
646 #Try getting informations
647 if str(_Data['r']) == 'groups':
648 #Name = str(_Data['name'])
649 #Dev_name = 'GROUP_' + Name.replace(' ','_')
650 #self.Devices[Dev_name] = {'id' : str(_Data['id']) , 'type' : 'groups' , 'model' : 'groups', 'state' : 'missing'}
651 self.SendCommand('/api/' + Parameters["Mode2"] + '/groups/' + str(_Data['id']) )
652
653 return
654 if state == 'banned':
655 Domoticz.Debug("Banned device > " + str(_Data['id']) + ' (' + str(_Data['r']) + ')')
656 return
657 if state == 'missing':
658 Domoticz.Error("Missing device > " + str(_Data['id']) + ' (' + str(_Data['r']) + ')')
659 return
660
661 model = self.Devices[IEEE].get('model','')
662
663 kwarg = {}
664
665 #MAJ State : _Data['e'] == 'changed'
666 if 'state' in _Data:
667 state = _Data['state']
668 kwarg.update(ProcessAllState(state , model))
669
670 if 'buttonevent' in state:
671 if model == 'XCube_C':
672 kwarg.update(ButtonconvertionXCUBE( state['buttonevent'] ) )
673 elif model == 'XCube_R':
674 kwarg.update(ButtonconvertionXCUBE_R( state['buttonevent'] ) )
675 elif model == 'Tradfri_remote':
676 kwarg.update(ButtonconvertionTradfriRemote( state['buttonevent'] ) )
677 elif model == 'Tradfri_on/off_switch':
678 kwarg.update(ButtonconvertionTradfriSwitch( state['buttonevent'] ) )
679 else:
680 kwarg.update(ButtonconvertionGeneric( state['buttonevent'] ) )
681 if IEEE not in self.NeedToReset:
682 self.NeedToReset.append(IEEE)
683
684 if 'vibration' in state:
685 kwarg.update(VibrationSensorConvertion( state['vibration'] , state.get('tiltangle',None)) )
686
687 if 'reachable' in state:
688 if state['reachable'] == True:
689 Unit = GetDomoDeviceInfo(IEEE)
690 #Jump following action if Unit content is not valid
691 if Unit != False:
692 LUpdate = Devices[Unit].LastUpdate
693 LUpdate=time.mktime(time.strptime(LUpdate,"%Y-%m-%d %H:%M:%S"))
694 current = time.time()
695
696 if (SETTODEFAULT):
697 #Check if the device has been see, at least 10 s ago
698 if (current-LUpdate) > 10:
699 Domoticz.Status("###### Device just re-connected : " + str(_Data) + "Set to defaut state")
700 self.SetDeviceDefautState(IEEE,_Data['r'])
701 else:
702 Domoticz.Status("###### Device just re-connected : " + str(_Data) + "But ignored")
703
704 if ('tampered' in state) or ('lowbattery' in state):
705 tampered = state.get('tampered',False)
706 lowbattery = state.get('lowbattery',False)
707 if tampered or lowbattery:
708 kwarg.update({'TimedOut':1})
709 Domoticz.Error("###### Device with hardware defaut : " + str(_Data))
710
711
712 #MAJ config
713 elif 'config' in _Data:
714 config = _Data['config']
715 kwarg.update(ProcessAllConfig(config))
716
717 else:
718 Domoticz.Error("Unknow MAJ" + str(_Data) )
719
720 if kwarg:
721 UpdateDevice(_Data['id'],_Data['r'],kwarg)
722
723 def DeleteDeviceFromdeCONZ(self,_id):
724
725 url = '/api/' + Parameters["Mode2"] + '/sensors/' + str(_id)
726
727 self.Buffer_Command.append((url,'delete'))
728 self.UpdateBuffer()
729
730 Domoticz.Status("### Deleting device " + str(_id))
731
732 def SendCommand(self,url,data=None):
733
734 Domoticz.Debug("Send Command " + url + " with " + str(data) + ' (' + str(len(self.Buffer_Command)) + ' in buffer)')
735
736 sendData = (url , data)
737 self.Buffer_Command.append(sendData)
738 self.UpdateBuffer()
739
740 def GetDevicedeCONZ(self,IEEE):
741 if IEEE in self.Devices:
742 return self.Devices[IEEE]['type'],self.Devices[IEEE]['id']
743
744 return False,False
745
746 def UpdateBuffer(self):
747 if len(self.Buffer_Command) == 0:
748 return
749
750 debut_time = time.time()
751
752 while len(self.Buffer_Command) > 0:
753
754 u , c = self.Buffer_Command.pop(0)
755
756 _Data = MakeRequest('http://' + Parameters["Address"] + ':' + Parameters["Port"] + str(u) , c)
757
758 #If not data usefull
759 if len(_Data) == 0:
760 return
761
762 #Clean data
763 #_Data = _Data.replace('true','True').replace('false','False').replace('null','None').replace('\n','***').replace('\00','')
764 try:
765 _Data = json.loads(_Data)
766 except:
767 #Sometime the connexion bug, trying to repair
768 Domoticz.Error("Malformed JSON response, Trying to repair : " + str(_Data) )
769 _Data = JSON_Repair(_Data)
770 try:
771 _Data = json.loads(_Data)
772 Domoticz.Error("New Data repaired : " + str(_Data))
773 except:
774 Domoticz.Error("Can't repair malformed JSON: " + str(_Data) )
775 _Data = None
776
777 #traitement
778 if not _Data == None: #WARNING None because can be {}
779 self.NormalConnexion(_Data)
780
781 fin_time = time.time()
782
783 # if the process take more than 1s, skip all, not normal
784 if fin_time - debut_time > 1 :
785 Domoticz.Error("Process request take too much time : " + str(fin_time - debut_time) + ' s')
786 break
787
788 return
789
790 def GetDeviceIEEE(self,_id,_type):
791 for IEEE in self.Devices:
792 if (self.Devices[IEEE]['type'] == _type) and (self.Devices[IEEE]['id'] == _id):
793 return IEEE,self.Devices[IEEE].get('state','unknow')
794
795 return False,'unknow'
796
797 def SetDeviceDefautState(self,IEEE,_type):
798 # Set bulb on same state than in domoticz
799 if _type == 'lights':
800 Unit = GetDomoDeviceInfo(IEEE)
801 if Devices[Unit].nValue == 0:
802 _json = '{"on":false}'
803 else:
804 _json = '{"on":true}'
805 dummy,deCONZ_ID = self.GetDevicedeCONZ(IEEE)
806 url = '/api/' + Parameters["Mode2"] + '/lights/' + str(deCONZ_ID) + '/state'
807 self.SendCommand(url,_json)
808
809global _plugin
810_plugin = BasePlugin()
811
812def onStart():
813 global _plugin
814 _plugin.onStart()
815
816def onStop():
817 global _plugin
818 _plugin.onStop()
819
820def onConnect(Connection, Status, Description):
821 global _plugin
822 _plugin.onConnect(Connection, Status, Description)
823
824def onMessage(Connection, Data):
825 global _plugin
826 _plugin.onMessage(Connection, Data)
827
828def onCommand(Unit, Command, Level, Hue):
829 global _plugin
830 _plugin.onCommand(Unit, Command, Level, Hue)
831
832def onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile):
833 global _plugin
834 _plugin.onNotification(Name, Subject, Text, Status, Priority, Sound, ImageFile)
835
836def onDisconnect(Connection):
837 global _plugin
838 _plugin.onDisconnect(Connection)
839
840def onHeartbeat():
841 global _plugin
842 _plugin.onHeartbeat()
843
844def onDeviceRemoved(unit):
845 global _plugin
846 _plugin.onDeviceRemoved(unit)
847
848 # Generic helper functions
849def DumpConfigToLog():
850 for x in Parameters:
851 if Parameters[x] != "":
852 Domoticz.Debug( "'" + x + "':'" + str(Parameters[x]) + "'")
853 Domoticz.Debug("Device count: " + str(len(Devices)))
854 for x in Devices:
855 Domoticz.Debug("Device: " + str(x) + " - " + str(Devices[x]))
856 Domoticz.Debug("Device ID: '" + str(Devices[x].ID) + "'")
857 Domoticz.Debug("Device Name: '" + Devices[x].Name + "'")
858 Domoticz.Debug("Device nValue: " + str(Devices[x].nValue))
859 Domoticz.Debug("Device sValue: '" + Devices[x].sValue + "'")
860 Domoticz.Debug("Device LastLevel: " + str(Devices[x].LastLevel))
861 return
862
863def GetDeviceIEEE(id,type):
864 global _plugin
865 return _plugin.GetDeviceIEEE(id,type)
866
867#*****************************************************************************************************
868
869def MakeRequest(url,param=None):
870
871 Domoticz.Debug("Making Request : " + url + ' with params ' + str(param) )
872
873 data = ''
874
875 try:
876 if not param == None:
877 if param == 'delete':
878 result=requests.delete(url, headers={'Content-Type': 'application/json' }, timeout=1)
879 else:
880 headers={'Content-Type': 'application/json' }
881 result=requests.put(url , headers=headers, json = param, timeout=1)
882 else :
883 result=requests.get(url, headers={'Content-Type': 'application/json' }, timeout=1)
884
885 if result.status_code == 200 :
886 data = result.content
887 else:
888 Domoticz.Error( "Connexion problem (1) with Gateway : " + str(result.status_code) )
889 return ''
890 except:
891 if not REQUESTPRESENT:
892 Domoticz.Error("Your pyton version miss requests library")
893 Domoticz.Error("To install it, type : sudo -H pip3 install requests | sudo -H pip install requests")
894 else:
895 try:
896 Domoticz.Error( "Connexion problem (2) with Gateway : " + str(result.status_code) )
897 except:
898 Domoticz.Error( "Connexion problem (3) with Gateway, check your API key")
899 return ''
900
901 Domoticz.Debug('Request Return : ' + str(data.decode("utf-8", "ignore")) )
902
903 return data.decode("utf-8", "ignore")
904
905def get_ip():
906 import socket
907 s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
908 try:
909 # doesn't even have to be reachable
910 s.connect(('10.255.255.255', 1))
911 IP = s.getsockname()[0]
912 except:
913 IP = '127.0.0.1'
914 finally:
915 s.close()
916 return IP
917
918def GetDomoDeviceInfo(IEEE):
919 for x in Devices:
920 if Devices[x].DeviceID == str(IEEE) :
921 return x
922 return False
923
924def FreeUnit() :
925 FreeUnit = ""
926 for x in range(1,256):
927 if x not in Devices :
928 FreeUnit=x
929 return FreeUnit
930 if FreeUnit == "" :
931 FreeUnit=len(Devices)+1
932 return FreeUnit
933
934def GetDomoUnit(_id,_type):
935 try:
936 IEEE,state = GetDeviceIEEE(_id,_type)
937
938 if IEEE == False:
939 Domoticz.Log("Device not in base, need resynchronisation ? > " + str(_id) + ' (' + str(_type) + ')')
940 return False
941 elif state == 'banned':
942 Domoticz.Log("Banned device > " + str(_id) + ' (' + str(_type) + ')')
943 return False
944 elif state == 'missing':
945 Domoticz.Log("missing device > " + str(_id) + ' (' + str(_type) + ')')
946 return False
947
948 return GetDomoDeviceInfo(IEEE)
949 except:
950 return False
951
952 return False
953
954def UpdateDevice(_id,_type,kwarg):
955
956 Unit = GetDomoUnit(_id,_type)
957
958 if not Unit or not kwarg:
959 Domoticz.Error("Can't Update Unit > " + str(_id) + ' (' + str(_type) + ')' )
960 return
961
962 #Check for special device.
963 if 'heatsetpoint' in kwarg:
964 v = kwarg.pop('heatsetpoint')
965 IEEE,dummy = GetDeviceIEEE(_id,_type)
966 Unit = GetDomoDeviceInfo(IEEE + '_heatsetpoint')
967 kwarg['nValue'] = 0
968 kwarg['nValue'] = str(v)
969
970 #Do we need to update the sensor ?
971 NeedUpdate = False
972
973 for a in kwarg:
974 if kwarg[a] != getattr(Devices[Unit], a ):
975 NeedUpdate = True
976 break
977
978 #Force update even there is no change, for exemple in case the user press a switch too fast, to not miss an event
979 # Only for switch > 'LevelNames' in Devices[Unit].Options
980 # Only sensors > _type == 'sensors'
981 if (('nValue' in kwarg) or ('sValue' in kwarg)) and ( ('LevelNames' in Devices[Unit].Options) and (kwarg['nValue'] != 0) ):
982 NeedUpdate = True
983
984 #Disabled because no update for battery or last seen for exemple
985 #No need to trigger in this situation
986 #if (kwarg['nValue'] == Devices[Unit].nValue) and (kwarg['nValue'] == Devices[Unit].nValue) and ('Color' not in kwarg):
987 # kwarg['SuppressTriggers'] = True
988
989 #Alaways update for Color Bulb
990 if 'Color' in kwarg:
991 NeedUpdate = True
992
993 #force update, at least 1 every 24h
994 if not NeedUpdate:
995 LUpdate = Devices[Unit].LastUpdate
996 LUpdate=time.mktime(time.strptime(LUpdate,"%Y-%m-%d %H:%M:%S"))
997 current = time.time()
998 if (current-LUpdate) > 86400:
999 NeedUpdate = True
1000
1001 #Theses value are needed for Domoticz
1002 if 'nValue' not in kwarg:
1003 kwarg['nValue'] = Devices[Unit].nValue
1004 if 'sValue' not in kwarg:
1005 kwarg['sValue'] = Devices[Unit].sValue
1006 if Devices[Unit].TimedOut != 0 and kwarg.get('TimedOut',0) == 0:
1007 NeedUpdate = True
1008 kwarg['TimedOut'] = 0
1009
1010 if NeedUpdate or not LIGHTLOG:
1011 Domoticz.Debug("### Update device ("+Devices[Unit].Name+") : " + str(kwarg))
1012 Devices[Unit].Update(**kwarg)
1013 else:
1014 Domoticz.Debug("### Update device ("+Devices[Unit].Name+") : " + str(kwarg) + ", IGNORED , no changes !")
1015
1016def CreateDevice(IEEE,_Name,_Type):
1017 kwarg = {}
1018 Unit = FreeUnit()
1019 TypeName = ''
1020
1021 #Operator
1022 if _Type == 'Color light' or _Type == 'Color dimmable light':
1023 kwarg['Type'] = 241
1024 kwarg['Subtype'] = 2
1025 kwarg['Switchtype'] = 7
1026
1027 elif _Type == 'Extended color light':
1028 kwarg['Type'] = 241
1029 kwarg['Subtype'] = 7
1030 kwarg['Switchtype'] = 7
1031
1032 elif _Type == 'Color temperature light':
1033 kwarg['Type'] = 241
1034 kwarg['Subtype'] = 8
1035 kwarg['Switchtype'] = 7
1036
1037 elif _Type == 'Dimmable light' or _Type == 'Dimmable plug-in unit' or _Type == 'Dimmer switch':
1038 kwarg['Type'] = 244
1039 kwarg['Subtype'] = 73
1040 kwarg['Switchtype'] = 7
1041
1042 elif _Type == 'Smart plug' or _Type == 'On/Off plug-in unit':
1043 kwarg['Type'] = 244
1044 kwarg['Subtype'] = 73
1045 kwarg['Switchtype'] = 0
1046 kwarg['Image'] = 1
1047
1048 elif _Type == 'On/Off light' or _Type == 'On/Off output' or _Type == 'On/Off light switch':
1049 kwarg['Type'] = 244
1050 kwarg['Subtype'] = 73
1051 kwarg['Switchtype'] = 0
1052
1053 elif _Type == 'Window covering device':
1054 kwarg['Type'] = 244
1055 kwarg['Subtype'] = 73
1056 kwarg['Switchtype'] = 16
1057
1058 elif _Type == 'Door Lock':
1059 kwarg['Type'] = 244
1060 kwarg['Subtype'] = 73
1061 kwarg['Switchtype'] = 0
1062
1063 #elif _Type == 'Fan':
1064
1065 #Sensors
1066 elif _Type == 'Daylight':
1067 kwarg['Type'] = 244
1068 kwarg['Subtype'] = 73
1069 kwarg['Switchtype'] = 9
1070
1071 elif _Type == 'ZHATemperature' or _Type == 'CLIPTemperature':
1072 kwarg['TypeName'] = 'Temperature'
1073
1074 elif _Type == 'ZHAHumidity' or _Type == 'CLIPHumidity':
1075 kwarg['TypeName'] = 'Humidity'
1076
1077 elif _Type == 'ZHAPressure'or _Type == 'CLIPPressure':
1078 kwarg['TypeName'] = 'Barometer'
1079
1080 elif _Type == 'ZHAOpenClose' or _Type == 'CLIPOpenClose':
1081 kwarg['Type'] = 244
1082 kwarg['Subtype'] = 73
1083 kwarg['Switchtype'] = 11
1084
1085 elif _Type == 'ZHAPresence' or _Type == 'CLIPPresence':
1086 kwarg['Type'] = 244
1087 kwarg['Subtype'] = 73
1088 kwarg['Switchtype'] = 8
1089
1090 elif _Type == 'ZHALightLevel' or _Type == 'CLIPLightLevel' or _Type == 'ZHALight':
1091 kwarg['TypeName'] = 'Illumination'
1092
1093 elif _Type == 'ZHAConsumption':# in kWh
1094 kwarg['TypeName'] = 'kWh'
1095
1096 elif _Type == 'ZHAPower':# in W
1097 kwarg['TypeName'] = 'Usage'
1098
1099 elif _Type == 'ZHAVibration':
1100 kwarg['Type'] = 244
1101 kwarg['Subtype'] = 62
1102 kwarg['Switchtype'] = 18
1103 kwarg['Options'] = {"LevelActions": "|||", "LevelNames": "Off|Vibrate|Rotation|drop", "LevelOffHidden": "true", "SelectorStyle": "0"}
1104
1105 elif _Type == 'ZHAThermostat' or _Type == 'CLIPThermostat':
1106 kwarg['Type'] = 242
1107 kwarg['Subtype'] = 1
1108
1109 elif _Type == 'ZHAAlarm':
1110 kwarg['Type'] = 244
1111 kwarg['Subtype'] = 73
1112 kwarg['Switchtype'] = 2
1113
1114 elif _Type == 'ZHAWater':
1115 kwarg['Type'] = 244
1116 kwarg['Subtype'] = 62
1117 kwarg['Switchtype'] = 5
1118 kwarg['Image'] = 11 # Visible only on floorplan
1119
1120 elif _Type == 'ZHAFire' or _Type == 'ZHACarbonMonoxide':
1121 kwarg['Type'] = 244
1122 kwarg['Subtype'] = 62
1123 kwarg['Switchtype'] = 5
1124
1125 elif _Type == 'CLIPGenericStatus':
1126 kwarg['TypeName'] = 'Text'
1127
1128 elif _Type == 'CLIPGenericFlag':
1129 kwarg['Type'] = 244
1130 kwarg['Subtype'] = 62
1131 kwarg['Switchtype'] = 0
1132 kwarg['Image'] = 9
1133
1134 #Switch
1135 elif _Type == 'Switch_Generic':
1136 kwarg['Type'] = 244
1137 kwarg['Subtype'] = 62
1138 kwarg['Switchtype'] = 18
1139 kwarg['Image'] = 9
1140 kwarg['Options'] = {"LevelActions": "||||||", "LevelNames": "Off|B1|B2|B3|B4|B5|B6|B7|B8", "LevelOffHidden": "true", "SelectorStyle": "0"}
1141
1142 elif _Type == 'Tradfri_remote':
1143 kwarg['Type'] = 244
1144 kwarg['Subtype'] = 62
1145 kwarg['Switchtype'] = 18
1146 kwarg['Image'] = 9
1147 kwarg['Options'] = {"LevelActions": "|||||", "LevelNames": "Off|On|More|Less|Right|Left", "LevelOffHidden": "true", "SelectorStyle": "0"}
1148
1149 elif _Type == 'Tradfri_on/off_switch':
1150 kwarg['Type'] = 244
1151 kwarg['Subtype'] = 62
1152 kwarg['Switchtype'] = 18
1153 kwarg['Options'] = {"LevelActions": "|||||", "LevelNames": "Off|B1C|B1L|B2C|B2L", "LevelOffHidden": "true", "SelectorStyle": "0"}
1154
1155 elif _Type == 'XCube_C':
1156 kwarg['Type'] = 244
1157 kwarg['Subtype'] = 62
1158 kwarg['Switchtype'] = 18
1159 kwarg['Image'] = 9
1160 kwarg['Options'] = {"LevelActions": "||||||||", "LevelNames": "Off|Shak|Wake|Drop|90°|180°|Push|Tap", "LevelOffHidden": "true", "SelectorStyle": "0"}
1161
1162 elif _Type == 'XCube_R':
1163 kwarg['TypeName'] = 'Custom'
1164 kwarg['Options'] = {"Custom": ("1;degree")}
1165
1166 #groups
1167 elif _Type == 'LightGroup' or _Type == 'groups':
1168 kwarg['Type'] = 241
1169 kwarg['Subtype'] = 7
1170 kwarg['Switchtype'] = 7
1171 #if 'bulbs_group' in Images:
1172 # kwarg['Image'] = Images['bulbs_group'].ID
1173
1174 #Scenes
1175 elif _Type == 'Scenes':
1176 kwarg['Type'] = 244
1177 kwarg['Subtype'] = 62
1178 kwarg['Switchtype'] = 9
1179 kwarg['Image'] = 9
1180
1181 else:
1182 Domoticz.Error("Unknow device type " + _Type )
1183 return
1184
1185 kwarg['DeviceID'] = IEEE
1186 kwarg['Name'] = _Name
1187 kwarg['Unit'] = Unit
1188 Domoticz.Device(**kwarg).Create()
1189
1190 Domoticz.Status("### Create Device " + IEEE + " > " + _Name + ' (' + _Type +') as Unit ' + str(Unit) )