forked from diyhue/diyHue
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathHueEmulator3.py
executable file
·1967 lines (1801 loc) · 141 KB
/
HueEmulator3.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
#!/usr/bin/python3
import argparse
import base64
import copy
import json
import logging
import os
import random
import socket
import ssl
import sys
import requests
import uuid
from collections import defaultdict
from datetime import datetime, timedelta
from http.server import BaseHTTPRequestHandler, HTTPServer
from socketserver import ThreadingMixIn
from subprocess import Popen, check_output, call
from threading import Thread
from time import sleep, strftime
from urllib.parse import parse_qs, urlparse
from functions import light_types, nextFreeId
from functions.colors import convert_rgb_xy, convert_xy, hsv_to_rgb
from functions.html import (description, webform_hue, webform_linkbutton,
webform_milight, webformDeconz, webformTradfri, lightsHttp)
from functions.ssdp import ssdpBroadcast, ssdpSearch
from functions.network import getIpAddress
from functions.docker import dockerSetup
from functions.entertainment import entertainmentService
from functions.email import sendEmail
from functions.request import sendRequest
from functions.lightRequest import sendLightRequest, syncWithLights
from functions.updateGroup import updateGroupStats
from protocols import protocols, yeelight, tasmota, native_single, native_multi, esphome
from functions.remoteApi import remoteApi
from functions.remoteDiscover import remoteDiscover
update_lights_on_startup = False # if set to true all lights will be updated with last know state on startup.
off_if_unreachable = False # If set to true all lights that unreachable are marked as off.
protocols = [yeelight, tasmota, native_single, native_multi, esphome]
ap = argparse.ArgumentParser()
# Arguements can also be passed as Environment Variables.
ap.add_argument("--ip", help="The IP address of the host system", type=str)
ap.add_argument("--http-port", help="The port to listen on for HTTP", type=int)
ap.add_argument("--mac", help="The MAC address of the host system", type=str)
ap.add_argument("--no-serve-https", action='store_true', help="Don't listen on port 443 with SSL")
ap.add_argument("--debug", action='store_true', help="Enables debug output")
ap.add_argument("--docker", action='store_true', help="Enables setup for use in docker container")
ap.add_argument("--ip-range", help="Set IP range for light discovery. Format: <START_IP>,<STOP_IP>", type=str)
ap.add_argument("--scan-on-host-ip", action='store_true', help="Scan the local IP address when discovering new lights")
ap.add_argument("--deconz", help="Provide the IP address of your Deconz host. 127.0.0.1 by default.", type=str)
ap.add_argument("--no-link-button", action='store_true', help="DANGEROUS! Don't require the link button to be pressed to pair the Hue app, just allow any app to connect")
ap.add_argument("--disable-online-discover", help="Disable Online and Remote API functions")
args = ap.parse_args()
if args.debug or (os.getenv('DEBUG') and (os.getenv('DEBUG') == "true" or os.getenv('DEBUG') == "True")):
root = logging.getLogger()
root.setLevel(logging.DEBUG)
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
root.addHandler(ch)
if args.ip:
HOST_IP = args.ip
elif os.getenv('IP'):
HOST_IP = os.getenv('IP')
else:
HOST_IP = getIpAddress()
if args.http_port:
HOST_HTTP_PORT = args.http_port
elif os.getenv('HTTP_PORT'):
HOST_HTTP_PORT = os.getenv('HTTP_PORT')
else:
HOST_HTTP_PORT = 80
HOST_HTTPS_PORT = 443 # Hardcoded for now
logging.info("Using Host %s:%s" % (HOST_IP, HOST_HTTP_PORT))
if args.mac:
dockerMAC = args.mac
mac = str(args.mac).replace(":","")
print("Host MAC given as " + mac)
elif os.getenv('MAC'):
dockerMAC = os.getenv('MAC')
mac = str(dockerMAC).replace(":","")
print("Host MAC given as " + mac)
else:
dockerMAC = check_output("cat /sys/class/net/$(ip -o addr | grep %s | awk '{print $2}')/address" % HOST_IP, shell=True).decode('utf-8')[:-1]
mac = check_output("cat /sys/class/net/$(ip -o addr | grep %s | awk '{print $2}')/address" % HOST_IP, shell=True).decode('utf-8').replace(":","")[:-1]
logging.info(mac)
if args.docker or (os.getenv('DOCKER') and os.getenv('DOCKER') == "true"):
print("Docker Setup Initiated")
docker = True
dockerSetup(dockerMAC)
print("Docker Setup Complete")
elif os.getenv('MAC'):
dockerMAC = os.getenv('MAC')
mac = str(dockerMAC).replace(":","")
print("Host MAC given as " + mac)
else:
docker = False
if args.ip_range:
ranges = args.ip_range.split(',')
if ranges[0] and int(ranges[0]) >= 0:
ip_range_start = int(ranges[0])
else:
ip_range_start = 0
if ranges[1] and int(ranges[1]) > 0:
ip_range_end = int(ranges[1])
else:
ip_range_end = 255
elif os.getenv('IP_RANGE'):
ranges = os.getenv('IP_RANGE').split(',')
if ranges[0] and int(ranges[0]) >= 0:
ip_range_start = int(ranges[0])
else:
ip_range_start = 0
if ranges[1] and int(ranges[1]) > 0:
ip_range_end = int(ranges[1])
else:
ip_range_end = 255
else:
ip_range_start = os.getenv('IP_RANGE_START', 0)
ip_range_end = os.getenv('IP_RANGE_END', 255)
logging.info("IP range for light discovery: "+str(ip_range_start)+"-"+str(ip_range_end))
if args.deconz:
deconz_ip = args.deconz
print("Deconz IP given as " + deconz_ip)
elif os.getenv('DECONZ'):
deconz_ip = os.getenv('DECONZ')
print("Deconz IP given as " + deconz_ip)
else:
deconz_ip = "127.0.0.1"
logging.info(deconz_ip)
if args.disable_online_discover or ((os.getenv('disable-online-discover') and (os.getenv('disable-online-discover') == "true" or os.getenv('disable-online-discover') == "True"))):
disableOnlineDiscover = True
logging.info("Online Discovery/Remote API Disabled!")
else:
disableOnlineDiscover = False
logging.info("Online Discovery/Remote API Enabled!")
cwd = os.path.split(os.path.abspath(__file__))[0]
def pretty_json(data):
return json.dumps(data, sort_keys=True, indent=4, separators=(',', ': '))
run_service = True
def initialize():
global bridge_config, new_lights, dxState
new_lights = {}
dxState = {"sensors": {}, "lights": {}, "groups": {}}
try:
path = cwd + '/config.json'
if os.path.exists(path):
bridge_config = load_config(path)
logging.info("Config loaded")
else:
logging.info("Config not found, creating new config from default settings")
bridge_config = load_config(cwd + '/default-config.json')
saveConfig()
except Exception:
logging.exception("CRITICAL! Config file was not loaded")
sys.exit(1)
ip_pices = HOST_IP.split(".")
bridge_config["config"]["ipaddress"] = HOST_IP
bridge_config["config"]["gateway"] = ip_pices[0] + "." + ip_pices[1] + "." + ip_pices[2] + ".1"
bridge_config["config"]["mac"] = mac[0] + mac[1] + ":" + mac[2] + mac[3] + ":" + mac[4] + mac[5] + ":" + mac[6] + mac[7] + ":" + mac[8] + mac[9] + ":" + mac[10] + mac[11]
bridge_config["config"]["bridgeid"] = (mac[:6] + 'FFFE' + mac[6:]).upper()
load_alarm_config()
generateDxState()
sanitizeBridgeScenes()
## generte security key for Hue Essentials remote access
if "Hue Essentials key" not in bridge_config["config"]:
bridge_config["config"]["Hue Essentials key"] = str(uuid.uuid1()).replace('-', '')
def sanitizeBridgeScenes():
for scene in list(bridge_config["scenes"]):
if "type" in bridge_config["scenes"][scene] and bridge_config["scenes"][scene]["type"] == "GroupScene": # scene has "type" key and "type" is "GroupScene"
if bridge_config["scenes"][scene]["group"] not in bridge_config["groups"]: # the group don't exist
del bridge_config["scenes"][scene] # delete the group
continue # avoid KeyError on next if statement
else:
for lightstate in list(bridge_config["scenes"][scene]["lightstates"]):
if lightstate not in bridge_config["groups"][bridge_config["scenes"][scene]["group"]]["lights"]: # if the light is no longer member in the group:
del bridge_config["scenes"][scene]["lightstates"][lightstate] # delete the lighstate of the missing light
else: # must be a lightscene
for lightstate in list(bridge_config["scenes"][scene]["lightstates"]):
if lightstate not in bridge_config["lights"]: # light is not present anymore on the bridge
del (bridge_config["scenes"][scene]["lightstates"][lightstate]) # delete invalid lightstate
if "lightstates" in bridge_config["scenes"][scene] and len(bridge_config["scenes"][scene]["lightstates"]) == 0: # empty scenes are useless
del bridge_config["scenes"][scene]
def getLightsVersions():
lights = {}
githubCatalog = json.loads(requests.get('https://raw.githubusercontent.com/diyhue/Lights/master/catalog.json').text)
for light in bridge_config["lights_address"].keys():
if bridge_config["lights_address"][light]["protocol"] in ["native_single", "native_multi"]:
if "light_nr" not in bridge_config["lights_address"][light] or bridge_config["lights_address"][light]["light_nr"] == 1:
currentData = json.loads(requests.get('http://' + bridge_config["lights_address"][light]["ip"] + '/detect', timeout=3).text)
lights[light] = {"name": currentData["name"], "currentVersion": currentData["version"], "lastVersion": githubCatalog[currentData["type"]]["version"], "firmware": githubCatalog[currentData["type"]]["filename"]}
return lights
def updateLight(light, filename):
firmware = requests.get('https://github.com/diyhue/Lights/raw/master/Arduino/bin/' + filename, allow_redirects=True)
open('/tmp/' + filename, 'wb').write(firmware.content)
file = {'update': open('/tmp/' + filename,'rb')}
update = requests.post('http://' + bridge_config["lights_address"][light]["ip"] + '/update', files=file)
# Make various updates to the config JSON structure to maintain backwards compatibility with old configs
def updateConfig():
#### bridge emulator config
if int(bridge_config["config"]["swversion"]) < 1935074050:
bridge_config["config"]["swversion"] = "1935074050"
bridge_config["config"]["apiversion"] = "1.35.0"
### end bridge config
if "emulator" not in bridge_config:
bridge_config["emulator"] = {"lights": {}, "sensors": {}}
if "Remote API enabled" not in bridge_config["config"]:
bridge_config["config"]["Remote API enabled"] = False
# Update deCONZ sensors
for sensor_id, sensor in bridge_config["deconz"]["sensors"].items():
if "modelid" not in sensor:
sensor["modelid"] = bridge_config["sensors"][sensor["bridgeid"]]["modelid"]
if sensor["modelid"] == "TRADFRI motion sensor":
if "lightsensor" not in sensor:
sensor["lightsensor"] = "internal"
# Update scenes
for scene_id, scene in bridge_config["scenes"].items():
if "type" not in scene:
scene["type"] = "LightGroup"
# Update sensors
for sensor_id, sensor in bridge_config["sensors"].items():
if sensor["type"] == "CLIPGenericStatus":
sensor["state"]["status"] = 0
elif sensor["type"] == "ZLLTemperature" and sensor["modelid"] == "SML001" and sensor["manufacturername"] == "Philips":
sensor["capabilities"] = {"certified": True, "primary": False}
sensor["swupdate"] = {"lastinstall": "2019-03-16T21:16:21","state": "noupdates"}
sensor["swversion"] = "6.1.1.27575"
elif sensor["type"] == "ZLLPresence" and sensor["modelid"] == "SML001" and sensor["manufacturername"] == "Philips":
sensor["capabilities"] = {"certified": True, "primary": True}
sensor["swupdate"] = {"lastinstall": "2019-03-16T21:16:21","state": "noupdates"}
sensor["swversion"] = "6.1.1.27575"
elif sensor["type"] == "ZLLLightLevel" and sensor["modelid"] == "SML001" and sensor["manufacturername"] == "Philips":
sensor["capabilities"] = {"certified": True, "primary": False}
sensor["swupdate"] = {"lastinstall": "2019-03-16T21:16:21","state": "noupdates"}
sensor["swversion"] = "6.1.1.27575"
# Update lights
for light_id, light_address in bridge_config["lights_address"].items():
light = bridge_config["lights"][light_id]
if light_address["protocol"] == "native" and "mac" not in light_address:
light_address["mac"] = light["uniqueid"][:17]
light["uniqueid"] = generate_unique_id()
# Update deCONZ protocol lights
if light_address["protocol"] == "deconz":
# Delete old keys
for key in list(light):
if key in ["hascolor", "ctmax", "ctmin", "etag"]:
del light[key]
if light["modelid"].startswith("TRADFRI"):
light.update({"manufacturername": "Philips", "swversion": "1.46.13_r26312"})
light["uniqueid"] = generate_unique_id()
if light["type"] == "Color temperature light":
light["modelid"] = "LTW001"
elif light["type"] == "Color light":
light["modelid"] = "LCT015"
light["type"] = "Extended color light"
elif light["type"] == "Dimmable light":
light["modelid"] = "LWB010"
# Update Philips lights firmware version
if "manufacturername" in light and light["manufacturername"] == "Philips":
swversion = "1.46.13_r26312"
if light["modelid"] in ["LST002", "LCT015", "LTW001", "LWB010"]:
# Update archetype for various Philips models
if light["modelid"] in ["LTW001", "LWB010"]:
archetype = "classicbulb"
light["productname"] = "Hue white lamp"
light["productid"] = "Philips-LWB014-1-A19DLv3"
light["capabilities"] = {"certified": True,"control": {"ct": {"max": 500,"min": 153},"maxlumen": 840,"mindimlevel": 5000},"streaming": {"proxy": False,"renderer": False}}
elif light["modelid"] == "LCT015":
archetype = "sultanbulb"
light["capabilities"] = {"certified": True,"control": {"colorgamut": [[0.6915,0.3083],[0.17,0.7],[0.1532,0.0475]],"colorgamuttype": "C","ct": {"max": 500,"min": 153},"maxlumen": 800,"mindimlevel": 1000},"streaming": {"proxy": True,"renderer": True}}
light["productname"] = "Hue color lamp"
elif light["modelid"] == "LST002":
archetype = "huelightstrip"
swversion = "5.127.1.26581"
light["capabilities"] = {"certified": True,"control": {"colorgamut": [[0.704,0.296],[0.2151,0.7106],[0.138,0.08]],"colorgamuttype": "A","maxlumen": 200,"mindimlevel": 10000},"streaming": {"proxy": False,"renderer": True}}
light["productname"] = "Hue lightstrip plus"
light["config"] = {"archetype": archetype, "function": "mixed", "direction": "omnidirectional"}
if "mode" in light["state"]:
light["state"]["mode"] = "homeautomation"
# Update startup config
if "startup" not in light["config"]:
light["config"]["startup"] = {"mode": "safety", "configured": False}
# Finally, update the software version
light["swversion"] = swversion
#set entertainment streaming to inactive on start/restart
for group_id, group in bridge_config["groups"].items():
if "type" in group and group["type"] == "Entertainment":
if "stream" not in group:
group["stream"] = {}
group["stream"].update({"active": False, "owner": None})
group["sensors"] = []
#fix timezones bug
if "values" not in bridge_config["capabilities"]["timezones"]:
timezones = bridge_config["capabilities"]["timezones"]
bridge_config["capabilities"]["timezones"] = {"values": timezones}
def addTradfriDimmer(sensor_id, group_id):
rules = [{ "actions":[{"address": "/groups/" + group_id + "/action", "body":{ "on":True, "bri":1 }, "method": "PUT" }], "conditions":[{ "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx"}, { "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "2002" }, { "address": "/groups/" + group_id + "/state/any_on", "operator": "eq", "value": "false" }], "name": "Remote " + sensor_id + " turn on" },{"actions":[{"address":"/groups/" + group_id + "/action", "body":{ "on": False}, "method":"PUT"}], "conditions":[{ "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }, { "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "4002" }, { "address": "/groups/" + group_id + "/state/any_on", "operator": "eq", "value": "true" }, { "address": "/groups/" + group_id + "/action/bri", "operator": "eq", "value": "1"}], "name":"Dimmer Switch " + sensor_id + " off"}, { "actions":[{ "address": "/groups/" + group_id + "/action", "body":{ "on":False }, "method": "PUT" }], "conditions":[{ "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }, { "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "3002" }, { "address": "/groups/" + group_id + "/state/any_on", "operator": "eq", "value": "true" }, { "address": "/groups/" + group_id + "/action/bri", "operator": "eq", "value": "1"}], "name": "Remote " + sensor_id + " turn off" }, { "actions": [{"address": "/groups/" + group_id + "/action", "body":{"bri_inc": 32, "transitiontime": 9}, "method": "PUT" }], "conditions": [{ "address": "/groups/" + group_id + "/state/any_on", "operator": "eq", "value": "true" },{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "2002" }, {"address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx"}], "name": "Dimmer Switch " + sensor_id + " rotate right"}, { "actions": [{"address": "/groups/" + group_id + "/action", "body":{"bri_inc": 56, "transitiontime": 9}, "method": "PUT" }], "conditions": [{ "address": "/groups/" + group_id + "/state/any_on", "operator": "eq", "value": "true" },{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "1002" }, {"address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx"}], "name": "Dimmer Switch " + sensor_id + " rotate fast right"}, {"actions": [{"address": "/groups/" + group_id + "/action", "body": {"bri_inc": -32, "transitiontime": 9}, "method": "PUT"}], "conditions": [{ "address": "/groups/" + group_id + "/action/bri", "operator": "gt", "value": "1"},{"address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "3002"}, {"address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx"}], "name": "Dimmer Switch " + sensor_id + " rotate left"}, {"actions": [{"address": "/groups/" + group_id + "/action", "body": {"bri_inc": -56, "transitiontime": 9}, "method": "PUT"}], "conditions": [{ "address": "/groups/" + group_id + "/action/bri", "operator": "gt", "value": "1"},{"address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "4002"}, {"address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx"}], "name": "Dimmer Switch " + sensor_id + " rotate left"}]
resourcelinkId = nextFreeId(bridge_config, "resourcelinks")
bridge_config["resourcelinks"][resourcelinkId] = {"classid": 15555,"description": "Rules for sensor " + sensor_id, "links": ["/sensors/" + sensor_id], "name": "Emulator rules " + sensor_id,"owner": list(bridge_config["config"]["whitelist"])[0]}
for rule in rules:
ruleId = nextFreeId(bridge_config, "rules")
bridge_config["rules"][ruleId] = rule
bridge_config["rules"][ruleId].update({"created": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"), "lasttriggered": None, "owner": list(bridge_config["config"]["whitelist"])[0], "recycle": True, "status": "enabled", "timestriggered": 0})
bridge_config["resourcelinks"][resourcelinkId]["links"].append("/rules/" + ruleId)
def addTradfriCtRemote(sensor_id, group_id):
rules = [{"actions": [{"address": "/groups/" + group_id + "/action","body": {"on": True},"method": "PUT"}],"conditions": [{"address": "/sensors/" + sensor_id + "/state/lastupdated","operator": "dx"},{"address": "/sensors/" + sensor_id + "/state/buttonevent","operator": "eq","value": "1002"},{"address": "/groups/" + group_id + "/state/any_on","operator": "eq","value": "false"}],"name": "Remote " + sensor_id + " button on"}, {"actions": [{"address": "/groups/" + group_id + "/action","body": {"on": False},"method": "PUT"}],"conditions": [{"address": "/sensors/" + sensor_id + "/state/lastupdated","operator": "dx"},{"address": "/sensors/" + sensor_id + "/state/buttonevent","operator": "eq","value": "1002"},{"address": "/groups/" + group_id + "/state/any_on","operator": "eq","value": "true"}],"name": "Remote " + sensor_id + " button off"},{ "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": 30, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "2002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " up-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": 56, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "2001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " up-long" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": -30, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "3002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " dn-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": -56, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "3001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " dn-long" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "ct_inc": 50, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "4002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ctl-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "ct_inc": 100, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "4001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ctl-long" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "ct_inc": -50, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "5002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ct-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "ct_inc": -100, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "5001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ct-long" }]
resourcelinkId = nextFreeId(bridge_config, "resourcelinks")
bridge_config["resourcelinks"][resourcelinkId] = {"classid": 15555,"description": "Rules for sensor " + sensor_id, "links": ["/sensors/" + sensor_id], "name": "Emulator rules " + sensor_id,"owner": list(bridge_config["config"]["whitelist"])[0]}
for rule in rules:
ruleId = nextFreeId(bridge_config, "rules")
bridge_config["rules"][ruleId] = rule
bridge_config["rules"][ruleId].update({"created": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"), "lasttriggered": None, "owner": list(bridge_config["config"]["whitelist"])[0], "recycle": True, "status": "enabled", "timestriggered": 0})
bridge_config["resourcelinks"][resourcelinkId]["links"].append("/rules/" + ruleId)
def addTradfriOnOffSwitch(sensor_id, group_id):
rules = [{"actions": [{"address": "/groups/" + group_id + "/action","body": {"on": True},"method": "PUT"}],"conditions": [{"address": "/sensors/" + sensor_id + "/state/lastupdated","operator": "dx"},{"address": "/sensors/" + sensor_id + "/state/buttonevent","operator": "eq","value": "1002"}],"name": "Remote " + sensor_id + " button on"}, {"actions": [{"address": "/groups/" + group_id + "/action","body": {"on": False},"method": "PUT"}],"conditions": [{"address": "/sensors/" + sensor_id + "/state/lastupdated","operator": "dx"},{"address": "/sensors/" + sensor_id + "/state/buttonevent","operator": "eq","value": "2002"}],"name": "Remote " + sensor_id + " button off"},{ "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": 30, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "2001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " up-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": -30, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "1001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " dn-press" }]
resourcelinkId = nextFreeId(bridge_config, "resourcelinks")
bridge_config["resourcelinks"][resourcelinkId] = {"classid": 15555,"description": "Rules for sensor " + sensor_id, "links": ["/sensors/" + sensor_id], "name": "Emulator rules " + sensor_id,"owner": list(bridge_config["config"]["whitelist"])[0]}
for rule in rules:
ruleId = nextFreeId(bridge_config, "rules")
bridge_config["rules"][ruleId] = rule
bridge_config["rules"][ruleId].update({"created": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"), "lasttriggered": None, "owner": list(bridge_config["config"]["whitelist"])[0], "recycle": True, "status": "enabled", "timestriggered": 0})
bridge_config["resourcelinks"][resourcelinkId]["links"].append("/rules/" + ruleId)
def addTradfriSceneRemote(sensor_id, group_id):
rules = [{"actions": [{"address": "/groups/" + group_id + "/action","body": {"on": True},"method": "PUT"}],"conditions": [{"address": "/sensors/" + sensor_id + "/state/lastupdated","operator": "dx"},{"address": "/sensors/" + sensor_id + "/state/buttonevent","operator": "eq","value": "1002"},{"address": "/groups/" + group_id + "/state/any_on","operator": "eq","value": "false"}],"name": "Remote " + sensor_id + " button on"}, {"actions": [{"address": "/groups/" + group_id + "/action","body": {"on": False},"method": "PUT"}],"conditions": [{"address": "/sensors/" + sensor_id + "/state/lastupdated","operator": "dx"},{"address": "/sensors/" + sensor_id + "/state/buttonevent","operator": "eq","value": "1002"},{"address": "/groups/" + group_id + "/state/any_on","operator": "eq","value": "true"}],"name": "Remote " + sensor_id + " button off"},{ "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": 30, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "2002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " up-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": 56, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "2001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " up-long" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": -30, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "3002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " dn-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "bri_inc": -56, "transitiontime": 9 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "3001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " dn-long" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "scene_inc": -1 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "4002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ctl-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "scene_inc": -1 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "4001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ctl-long" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "scene_inc": 1 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "5002" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ct-press" }, { "actions": [{ "address": "/groups/" + group_id + "/action", "body": { "scene_inc": 1 }, "method": "PUT" }], "conditions": [{ "address": "/sensors/" + sensor_id + "/state/buttonevent", "operator": "eq", "value": "5001" }, { "address": "/sensors/" + sensor_id + "/state/lastupdated", "operator": "dx" }], "name": "Dimmer Switch " + sensor_id + " ct-long" }]
resourcelinkId = nextFreeId(bridge_config, "resourcelinks")
bridge_config["resourcelinks"][resourcelinkId] = {"classid": 15555,"description": "Rules for sensor " + sensor_id, "links": ["/sensors/" + sensor_id], "name": "Emulator rules " + sensor_id,"owner": list(bridge_config["config"]["whitelist"])[0]}
for rule in rules:
ruleId = nextFreeId(bridge_config, "rules")
bridge_config["rules"][ruleId] = rule
bridge_config["rules"][ruleId].update({"created": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"), "lasttriggered": None, "owner": list(bridge_config["config"]["whitelist"])[0], "recycle": True, "status": "enabled", "timestriggered": 0})
bridge_config["resourcelinks"][resourcelinkId]["links"].append("/rules/" + ruleId)
def addHueMotionSensor(uniqueid, name="Hue motion sensor"):
new_sensor_id = nextFreeId(bridge_config, "sensors")
if uniqueid == "":
uniqueid = "00:17:88:01:02:"
if len(new_sensor_id) == 1:
uniqueid += "0" + new_sensor_id
else:
uniqueid += new_sensor_id
bridge_config["sensors"][nextFreeId(bridge_config, "sensors")] = {"name": "Hue temperature sensor 1", "uniqueid": uniqueid + ":d0:5b-02-0402", "type": "ZLLTemperature", "swversion": "6.1.0.18912", "state": {"temperature": None, "lastupdated": "none"}, "manufacturername": "Philips", "config": {"on": False, "battery": 100, "reachable": True, "alert":"none", "ledindication": False, "usertest": False, "pending": []}, "modelid": "SML001"}
motion_sensor = nextFreeId(bridge_config, "sensors")
bridge_config["sensors"][motion_sensor] = {"name": name, "uniqueid": uniqueid + ":d0:5b-02-0406", "type": "ZLLPresence", "swversion": "6.1.0.18912", "state": {"lastupdated": "none", "presence": None}, "manufacturername": "Philips", "config": {"on": False,"battery": 100,"reachable": True, "alert": "lselect", "ledindication": False, "usertest": False, "sensitivity": 2, "sensitivitymax": 2,"pending": []}, "modelid": "SML001"}
bridge_config["sensors"][nextFreeId(bridge_config, "sensors")] = {"name": "Hue ambient light sensor", "uniqueid": uniqueid + ":d0:5b-02-0400", "type": "ZLLLightLevel", "swversion": "6.1.0.18912", "state": {"dark": True, "daylight": False, "lightlevel": 6000, "lastupdated": "none"}, "manufacturername": "Philips", "config": {"on": False,"battery": 100, "reachable": True, "alert": "none", "tholddark": 21597, "tholdoffset": 7000, "ledindication": False, "usertest": False, "pending": []}, "modelid": "SML001"}
return(motion_sensor)
def addHueSwitch(uniqueid, sensorsType):
new_sensor_id = nextFreeId(bridge_config, "sensors")
if uniqueid == "":
uniqueid = "00:17:88:01:02:"
if len(new_sensor_id) == 1:
uniqueid += "0" + new_sensor_id + ":4d:c6-02-fc00"
else:
uniqueid += new_sensor_id + ":4d:c6-02-fc00"
bridge_config["sensors"][new_sensor_id] = {"state": {"buttonevent": 0, "lastupdated": "none"}, "config": {"on": True, "battery": 100, "reachable": True}, "name": "Dimmer Switch" if sensorsType == "ZLLSwitch" else "Tap Switch", "type": sensorsType, "modelid": "RWL021" if sensorsType == "ZLLSwitch" else "ZGPSWITCH", "manufacturername": "Philips", "swversion": "5.45.1.17846" if sensorsType == "ZLLSwitch" else "", "uniqueid": uniqueid}
return(new_sensor_id)
#load config files
def load_config(path):
with open(path, 'r', encoding="utf-8") as fp:
return json.load(fp)
def resourceRecycle():
sleep(5) #give time to application to delete all resources, then start the cleanup
resourcelinks = {"groups": [],"lights": [], "sensors": [], "rules": [], "scenes": [], "schedules": [], "resourcelinks": []}
for resourcelink in bridge_config["resourcelinks"].keys():
for link in bridge_config["resourcelinks"][resourcelink]["links"]:
link_parts = link.split("/")
resourcelinks[link_parts[1]].append(link_parts[2])
for resource in resourcelinks.keys():
for key in list(bridge_config[resource]):
if "recycle" in bridge_config[resource][key] and bridge_config[resource][key]["recycle"] and key not in resourcelinks[resource]:
logging.info("delete " + resource + " / " + key)
del bridge_config[resource][key]
def load_alarm_config(): #load and configure alarm virtual light
if bridge_config["alarm_config"]["mail_username"] != "":
logging.info("E-mail account configured")
if "virtual_light" not in bridge_config["alarm_config"]:
logging.info("Send test email")
if sendEmail(bridge_config["alarm_config"], "dummy test"):
logging.info("Mail succesfully sent\nCreate alarm virtual light")
new_light_id = nextFreeId(bridge_config, "lights")
bridge_config["lights"][new_light_id] = {"state": {"on": False, "bri": 200, "hue": 0, "sat": 0, "xy": [0.690456, 0.295907], "ct": 461, "alert": "none", "effect": "none", "colormode": "xy", "reachable": True}, "type": "Extended color light", "name": "Alarm", "uniqueid": "1234567ffffff", "modelid": "LLC012", "swversion": "66009461"}
bridge_config["alarm_config"]["virtual_light"] = new_light_id
else:
logging.info("Mail test failed")
def saveConfig(filename='config.json'):
with open(cwd + '/' + filename, 'w', encoding="utf-8") as fp:
json.dump(bridge_config, fp, sort_keys=True, indent=4, separators=(',', ': '))
if docker:
Popen(["cp", cwd + '/' + filename, cwd + '/' + 'export/'])
def generateDxState():
for sensor in bridge_config["sensors"]:
if sensor not in dxState["sensors"] and "state" in bridge_config["sensors"][sensor]:
dxState["sensors"][sensor] = {"state": {}}
for key in bridge_config["sensors"][sensor]["state"].keys():
if key in ["lastupdated", "presence", "flag", "dark", "daylight", "status"]:
dxState["sensors"][sensor]["state"].update({key: datetime.now()})
for group in bridge_config["groups"]:
if group not in dxState["groups"] and "state" in bridge_config["groups"][group]:
dxState["groups"][group] = {"state": {}}
for key in bridge_config["groups"][group]["state"].keys():
dxState["groups"][group]["state"].update({key: datetime.now()})
for light in bridge_config["lights"]:
if light not in dxState["lights"] and "state" in bridge_config["lights"][light]:
dxState["lights"][light] = {"state": {}}
for key in bridge_config["lights"][light]["state"].keys():
if key in ["on", "bri", "colormode", "reachable"]:
dxState["lights"][light]["state"].update({key: datetime.now()})
def schedulerProcessor():
while run_service:
for schedule in bridge_config["schedules"].keys():
try:
delay = 0
if bridge_config["schedules"][schedule]["status"] == "enabled":
if bridge_config["schedules"][schedule]["localtime"][-9:-8] == "A":
delay = random.randrange(0, int(bridge_config["schedules"][schedule]["localtime"][-8:-6]) * 3600 + int(bridge_config["schedules"][schedule]["localtime"][-5:-3]) * 60 + int(bridge_config["schedules"][schedule]["localtime"][-2:]))
schedule_time = bridge_config["schedules"][schedule]["localtime"][:-9]
else:
schedule_time = bridge_config["schedules"][schedule]["localtime"]
if schedule_time.startswith("W"):
pices = schedule_time.split('/T')
if int(pices[0][1:]) & (1 << 6 - datetime.today().weekday()):
if pices[1] == datetime.now().strftime("%H:%M:%S"):
logging.info("execute schedule: " + schedule + " withe delay " + str(delay))
sendRequest(bridge_config["schedules"][schedule]["command"]["address"], bridge_config["schedules"][schedule]["command"]["method"], json.dumps(bridge_config["schedules"][schedule]["command"]["body"]), 1, delay)
elif schedule_time.startswith("PT"):
timmer = schedule_time[2:]
(h, m, s) = timmer.split(':')
d = timedelta(hours=int(h), minutes=int(m), seconds=int(s))
if bridge_config["schedules"][schedule]["starttime"] == (datetime.utcnow() - d).replace(microsecond=0).isoformat():
logging.info("execute timmer: " + schedule + " withe delay " + str(delay))
sendRequest(bridge_config["schedules"][schedule]["command"]["address"], bridge_config["schedules"][schedule]["command"]["method"], json.dumps(bridge_config["schedules"][schedule]["command"]["body"]), 1, delay)
bridge_config["schedules"][schedule]["status"] = "disabled"
elif schedule_time.startswith("R/PT"):
timmer = schedule_time[4:]
(h, m, s) = timmer.split(':')
d = timedelta(hours=int(h), minutes=int(m), seconds=int(s))
print("#### " + bridge_config["schedules"][schedule]["starttime"] + " vs " + (datetime.utcnow() - d).replace(microsecond=0).isoformat())
if bridge_config["schedules"][schedule]["starttime"] == (datetime.utcnow() - d).replace(microsecond=0).isoformat():
logging.info("execute timmer: " + schedule + " withe delay " + str(delay))
bridge_config["schedules"][schedule]["starttime"] = datetime.utcnow().replace(microsecond=0).isoformat()
sendRequest(bridge_config["schedules"][schedule]["command"]["address"], bridge_config["schedules"][schedule]["command"]["method"], json.dumps(bridge_config["schedules"][schedule]["command"]["body"]), 1, delay)
else:
if schedule_time == datetime.now().strftime("%Y-%m-%dT%H:%M:%S"):
logging.info("execute schedule: " + schedule + " withe delay " + str(delay))
sendRequest(bridge_config["schedules"][schedule]["command"]["address"], bridge_config["schedules"][schedule]["command"]["method"], json.dumps(bridge_config["schedules"][schedule]["command"]["body"]), 1, delay)
if bridge_config["schedules"][schedule]["autodelete"]:
del bridge_config["schedules"][schedule]
except Exception as e:
logging.info("Exception while processing the schedule " + schedule + " | " + str(e))
if (datetime.now().strftime("%M:%S") == "00:10"): #auto save configuration every hour
saveConfig()
Thread(target=daylightSensor).start()
if (datetime.now().strftime("%H") == "23" and datetime.now().strftime("%A") == "Sunday"): #backup config every Sunday at 23:00:10
if docker:
saveConfig("export/config-backup-" + datetime.now().strftime("%Y-%m-%d") + ".json")
else:
saveConfig("config-backup-" + datetime.now().strftime("%Y-%m-%d") + ".json")
sleep(1)
def switchScene(group, direction):
group_scenes = []
current_position = -1
possible_current_position = -1 # used in case the brigtness was changes and will be no perfect match (scene lightstates vs light states)
break_next = False
for scene in bridge_config["scenes"]:
if bridge_config["groups"][group]["lights"][0] in bridge_config["scenes"][scene]["lights"]:
group_scenes.append(scene)
if break_next: # don't lose time as this is the scene we need
break
is_current_scene = True
is_possible_current_scene = True
for light in bridge_config["scenes"][scene]["lightstates"]:
for key in bridge_config["scenes"][scene]["lightstates"][light].keys():
if key == "xy":
if not bridge_config["scenes"][scene]["lightstates"][light]["xy"][0] == bridge_config["lights"][light]["state"]["xy"][0] and not bridge_config["scenes"][scene]["lightstates"][light]["xy"][1] == bridge_config["lights"][light]["state"]["xy"][1]:
is_current_scene = False
else:
if not bridge_config["scenes"][scene]["lightstates"][light][key] == bridge_config["lights"][light]["state"][key]:
is_current_scene = False
if not key == "bri":
is_possible_current_scene = False
if is_current_scene:
current_position = len(group_scenes) -1
if direction == -1 and len(group_scenes) != 1:
break
elif len(group_scenes) != 1:
break_next = True
elif is_possible_current_scene:
possible_current_position = len(group_scenes) -1
matched_scene = ""
if current_position + possible_current_position == -2:
logging.info("current scene not found, reset to zero")
if len(group_scenes) != 0:
matched_scene = group_scenes[0]
else:
logging.info("error, no scenes found")
return
elif current_position != -1:
if len(group_scenes) -1 < current_position + direction:
matched_scene = group_scenes[0]
else:
matched_scene = group_scenes[current_position + direction]
elif possible_current_position != -1:
if len(group_scenes) -1 < possible_current_position + direction:
matched_scene = group_scenes[0]
else:
matched_scene = group_scenes[possible_current_position + direction]
logging.info("matched scene " + bridge_config["scenes"][matched_scene]["name"])
for light in bridge_config["scenes"][matched_scene]["lights"]:
bridge_config["lights"][light]["state"].update(bridge_config["scenes"][matched_scene]["lightstates"][light])
if "xy" in bridge_config["scenes"][matched_scene]["lightstates"][light]:
bridge_config["lights"][light]["state"]["colormode"] = "xy"
elif "ct" in bridge_config["scenes"][matched_scene]["lightstates"][light]:
bridge_config["lights"][light]["state"]["colormode"] = "ct"
elif "hue" or "sat" in bridge_config["scenes"][matched_scene]["lightstates"][light]:
bridge_config["lights"][light]["state"]["colormode"] = "hs"
sendLightRequest(light, bridge_config["scenes"][matched_scene]["lightstates"][light], bridge_config["lights"], bridge_config["lights_address"])
updateGroupStats(light, bridge_config["lights"], bridge_config["groups"])
def checkRuleConditions(rule, device, current_time, ignore_ddx=False):
ddx = 0
device_found = False
ddx_sensor = []
for condition in bridge_config["rules"][rule]["conditions"]:
try:
url_pices = condition["address"].split('/')
if url_pices[1] == device[0] and url_pices[2] == device[1]:
device_found = True
if condition["operator"] == "eq":
if condition["value"] == "true":
if not bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]:
return [False, 0]
elif condition["value"] == "false":
if bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]:
return [False, 0]
else:
if not int(bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]) == int(condition["value"]):
return [False, 0]
elif condition["operator"] == "gt":
if not int(bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]) > int(condition["value"]):
return [False, 0]
elif condition["operator"] == "lt":
if not int(bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]) < int(condition["value"]):
return [False, 0]
elif condition["operator"] == "dx":
if not dxState[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]] == current_time:
return [False, 0]
elif condition["operator"] == "in":
periods = condition["value"].split('/')
if condition["value"][0] == "T":
timeStart = datetime.strptime(periods[0], "T%H:%M:%S").time()
timeEnd = datetime.strptime(periods[1], "T%H:%M:%S").time()
now_time = datetime.now().time()
if timeStart < timeEnd:
if not timeStart <= now_time <= timeEnd:
return [False, 0]
else:
if not (timeStart <= now_time or now_time <= timeEnd):
return [False, 0]
elif condition["operator"] == "ddx" and ignore_ddx is False:
if not dxState[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]] == current_time:
return [False, 0]
else:
ddx = int(condition["value"][2:4]) * 3600 + int(condition["value"][5:7]) * 60 + int(condition["value"][-2:])
ddx_sensor = url_pices
except Exception as e:
logging.info("rule " + rule + " failed, reason:" + str(e))
if device_found:
return [True, ddx, ddx_sensor]
else:
return [False]
def ddxRecheck(rule, device, current_time, ddx_delay, ddx_sensor):
for x in range(ddx_delay):
if current_time != dxState[ddx_sensor[1]][ddx_sensor[2]][ddx_sensor[3]][ddx_sensor[4]]:
logging.info("ddx rule " + rule + " canceled after " + str(x) + " seconds")
return # rule not valid anymore because sensor state changed while waiting for ddx delay
sleep(1)
current_time = datetime.now()
rule_state = checkRuleConditions(rule, device, current_time, True)
if rule_state[0]: #if all conditions are meet again
logging.info("delayed rule " + rule + " is triggered")
bridge_config["rules"][rule]["lasttriggered"] = current_time.strftime("%Y-%m-%dT%H:%M:%S")
bridge_config["rules"][rule]["timestriggered"] += 1
for action in bridge_config["rules"][rule]["actions"]:
sendRequest("/api/" + bridge_config["rules"][rule]["owner"] + action["address"], action["method"], json.dumps(action["body"]))
def rulesProcessor(device, current_time):
bridge_config["config"]["localtime"] = current_time.strftime("%Y-%m-%dT%H:%M:%S") #required for operator dx to address /config/localtime
actionsToExecute = []
for rule in bridge_config["rules"].keys():
if bridge_config["rules"][rule]["status"] == "enabled":
rule_result = checkRuleConditions(rule, device, current_time)
if rule_result[0]:
if rule_result[1] == 0: #is not ddx rule
logging.info("rule " + rule + " is triggered")
bridge_config["rules"][rule]["lasttriggered"] = current_time.strftime("%Y-%m-%dT%H:%M:%S")
bridge_config["rules"][rule]["timestriggered"] += 1
for action in bridge_config["rules"][rule]["actions"]:
actionsToExecute.append(action)
else: #if ddx rule
logging.info("ddx rule " + rule + " will be re validated after " + str(rule_result[1]) + " seconds")
Thread(target=ddxRecheck, args=[rule, device, current_time, rule_result[1], rule_result[2]]).start()
for action in actionsToExecute:
sendRequest("/api/" + list(bridge_config["config"]["whitelist"])[0] + action["address"], action["method"], json.dumps(action["body"]))
def scanHost(host, port):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.02) # Very short timeout. If scanning fails this could be increased
result = sock.connect_ex((host, port))
sock.close()
return result
def iter_ips(port):
host = HOST_IP.split('.')
if args.scan_on_host_ip:
yield ('127.0.0.1', port)
return
for addr in range(ip_range_start, ip_range_end + 1):
host[3] = str(addr)
test_host = '.'.join(host)
if test_host != HOST_IP:
yield (test_host, port)
def find_hosts(port):
validHosts = []
for host, port in iter_ips(port):
if scanHost(host, port) == 0:
hostWithPort = '%s:%s' % (host, port)
validHosts.append(hostWithPort)
return validHosts
def find_light_in_config_from_mac_and_nr(bridge_config, mac_address, light_nr):
for light_id, light_address in bridge_config["lights_address"].items():
if (light_address["protocol"] in ["native", "native_single", "native_multi"]
and light_address["mac"] == mac_address
and ('light_nr' not in light_address or
light_address['light_nr'] == light_nr)):
return light_id
return None
def find_light_in_config_from_uid(bridge_config, unique_id):
for light in bridge_config["lights"].keys():
if bridge_config["lights"][light]["uniqueid"] == unique_id:
return light
return None
def generate_light_name(base_name, light_nr):
# Light name can only contain 32 characters
suffix = ' %s' % light_nr
return '%s%s' % (base_name[:32-len(suffix)], suffix)
def generate_unique_id():
rand_bytes = [random.randrange(0, 256) for _ in range(3)]
return "00:17:88:01:00:%02x:%02x:%02x-0b" % (rand_bytes[0],rand_bytes[1],rand_bytes[2])
def scan_for_lights(): #scan for ESP8266 lights and strips
Thread(target=yeelight.discover, args=[bridge_config, new_lights]).start()
Thread(target=tasmota.discover, args=[bridge_config, new_lights]).start()
Thread(target=esphome.discover, args=[bridge_config, new_lights]).start()
#return all host that listen on port 80
device_ips = find_hosts(80)
logging.info(pretty_json(device_ips))
#logging.debug('devs', device_ips)
for ip in device_ips:
try:
response = requests.get("http://" + ip + "/detect", timeout=3)
if response.status_code == 200:
# XXX JSON validation
try:
device_data = json.loads(response.text)
logging.info(pretty_json(device_data))
if "modelid" in device_data:
logging.info(ip + " is " + device_data['name'])
if "protocol" in device_data:
protocol = device_data["protocol"]
else:
protocol = "native"
# Get number of lights
lights = 1
if "lights" in device_data:
lights = device_data["lights"]
# Add each light to config
logging.info("Add new light: " + device_data["name"])
for x in range(1, lights + 1):
light = find_light_in_config_from_mac_and_nr(bridge_config,
device_data['mac'], x)
# Try to find light in existing config
if light:
logging.info("Updating old light: " + device_data["name"])
# Light found, update config
light_address = bridge_config["lights_address"][light]
light_address["ip"] = ip
light_address["protocol"] = protocol
if "version" in device_data:
light_address.update({
"version": device_data["version"],
"type": device_data["type"],
"name": device_data["name"]
})
continue
new_light_id = nextFreeId(bridge_config, "lights")
light_name = generate_light_name(device_data['name'], x)
# Construct the configuration for this light from a few sources, in order of precedence
# (later sources override earlier ones).
# Global defaults
new_light = {
"manufacturername": "Philips",
"uniqueid": generate_unique_id(),
}
# Defaults for this specific modelid
if device_data["modelid"] in light_types:
new_light.update(light_types[device_data["modelid"]])
# Make sure to make a copy of the state dictionary so we don't share the dictionary
new_light['state'] = light_types[device_data["modelid"]]['state'].copy()
# Overrides from the response JSON
new_light["modelid"] = device_data["modelid"]
new_light["name"] = light_name
# Add the light to new lights, and to bridge_config (in two places)
new_lights[new_light_id] = {"name": light_name}
bridge_config["lights"][new_light_id] = new_light
bridge_config["lights_address"][new_light_id] = {
"ip": ip,
"light_nr": x,
"protocol": protocol,
"mac": device_data["mac"]
}
except ValueError:
logging.info('Decoding JSON from %s has failed', ip)
except Exception as e:
logging.info("ip %s is unknown device: %s", ip, e)
#raise
scanDeconz()
scanTradfri()
saveConfig()
def longPressButton(sensor, buttonevent):
logging.info("long press detected")
sleep(1)
while bridge_config["sensors"][sensor]["state"]["buttonevent"] == buttonevent:
logging.info("still pressed")
current_time = datetime.now()
dxState["sensors"][sensor]["state"]["lastupdated"] = current_time
rulesProcessor(["sensors",sensor], current_time)
sleep(0.5)
return
def motionDetected(sensor):
logging.info("monitoring motion sensor " + sensor)
while bridge_config["sensors"][sensor]["state"]["presence"] == True:
if datetime.utcnow() - datetime.strptime(bridge_config["sensors"][sensor]["state"]["lastupdated"], "%Y-%m-%dT%H:%M:%S") > timedelta(seconds=30):
bridge_config["sensors"][sensor]["state"]["presence"] = False
bridge_config["sensors"][sensor]["state"]["lastupdated"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")
current_time = datetime.now()
dxState["sensors"][sensor]["state"]["presence"] = current_time
rulesProcessor(["sensors",sensor], current_time)
sleep(1)
logging.info("set motion sensor " + sensor + " to motion = False")
return
def scanTradfri():
if "tradfri" in bridge_config:
tradri_devices = json.loads(check_output("./coap-client-linux -m get -u \"" + bridge_config["tradfri"]["identity"] + "\" -k \"" + bridge_config["tradfri"]["psk"] + "\" \"coaps://" + bridge_config["tradfri"]["ip"] + ":5684/15001\"", shell=True).decode('utf-8').rstrip('\n').split("\n")[-1])
logging.info(pretty_json(tradri_devices))
lights_found = 0
for device in tradri_devices:
device_parameters = json.loads(check_output("./coap-client-linux -m get -u \"" + bridge_config["tradfri"]["identity"] + "\" -k \"" + bridge_config["tradfri"]["psk"] + "\" \"coaps://" + bridge_config["tradfri"]["ip"] + ":5684/15001/" + str(device) +"\"", shell=True).decode('utf-8').rstrip('\n').split("\n")[-1])
if "3311" in device_parameters:
new_light = True
for light in bridge_config["lights_address"]:
if bridge_config["lights_address"][light]["protocol"] == "ikea_tradfri" and bridge_config["lights_address"][light]["device_id"] == device:
new_light = False
break
if new_light:
lights_found += 1
#register new tradfri lightdevice_id
logging.info("register tradfi light " + device_parameters["9001"])
new_light_id = nextFreeId(bridge_config, "lights")
bridge_config["lights"][new_light_id] = {"state": {"on": False, "bri": 200, "hue": 0, "sat": 0, "xy": [0.0, 0.0], "ct": 461, "alert": "none", "effect": "none", "colormode": "ct", "reachable": True}, "type": "Extended color light", "name": device_parameters["9001"], "uniqueid": "1234567" + str(device), "modelid": "LLM010", "swversion": "66009461", "manufacturername": "Philips"}
new_lights.update({new_light_id: {"name": device_parameters["9001"]}})
bridge_config["lights_address"][new_light_id] = {"device_id": device, "preshared_key": bridge_config["tradfri"]["psk"], "identity": bridge_config["tradfri"]["identity"], "ip": bridge_config["tradfri"]["ip"], "protocol": "ikea_tradfri"}
return lights_found
else:
return 0
def websocketClient():
from ws4py.client.threadedclient import WebSocketClient
class EchoClient(WebSocketClient):
def opened(self):
self.send("hello")
def closed(self, code, reason=None):
logging.info(("deconz websocket disconnected", code, reason))
del bridge_config["deconz"]["websocketport"]
def received_message(self, m):
logging.info(m)
message = json.loads(str(m))
try:
if message["r"] == "sensors":
bridge_sensor_id = bridge_config["deconz"]["sensors"][message["id"]]["bridgeid"]
if "state" in message and bridge_config["sensors"][bridge_sensor_id]["config"]["on"]:
#change codes for emulated hue Switches
if "hueType" in bridge_config["deconz"]["sensors"][message["id"]]:
rewriteDict = {"ZGPSwitch": {1002: 34, 3002: 16, 4002: 17, 5002: 18}, "ZLLSwitch" : {1002 : 1000, 2002: 2000, 2001: 2001, 2003: 2002, 3001: 3001, 3002: 3000, 3003: 3002, 4002: 4000, 5002: 4000} }
message["state"]["buttonevent"] = rewriteDict[bridge_config["deconz"]["sensors"][message["id"]]["hueType"]][message["state"]["buttonevent"]]
#end change codes for emulated hue Switches
#convert tradfri motion sensor notification to look like Hue Motion Sensor
if message["state"] and bridge_config["deconz"]["sensors"][message["id"]]["modelid"] == "TRADFRI motion sensor":
#find the light sensor id
light_sensor = "0"
for sensor in bridge_config["sensors"].keys():
if bridge_config["sensors"][sensor]["type"] == "ZLLLightLevel" and bridge_config["sensors"][sensor]["uniqueid"] == bridge_config["sensors"][bridge_sensor_id]["uniqueid"][:-1] + "0":
light_sensor = sensor
break
if bridge_config["deconz"]["sensors"][message["id"]]["lightsensor"] == "none":
message["state"].update({"dark": True})
elif bridge_config["deconz"]["sensors"][message["id"]]["lightsensor"] == "astral":
message["state"]["dark"] = not bridge_config["sensors"]["1"]["state"]["daylight"]
elif bridge_config["deconz"]["sensors"][message["id"]]["lightsensor"] == "combined":
if not bridge_config["sensors"]["1"]["state"]["daylight"]:
message["state"]["dark"] = True
elif (datetime.strptime(message["state"]["lastupdated"], "%Y-%m-%dT%H:%M:%S") - datetime.strptime(bridge_config["sensors"][light_sensor]["state"]["lastupdated"], "%Y-%m-%dT%H:%M:%S")).total_seconds() > 1200:
message["state"]["dark"] = False
if message["state"]["dark"]:
bridge_config["sensors"][light_sensor]["state"]["lightlevel"] = 6000
else:
bridge_config["sensors"][light_sensor]["state"]["lightlevel"] = 25000
bridge_config["sensors"][light_sensor]["state"]["dark"] = message["state"]["dark"]
bridge_config["sensors"][light_sensor]["state"]["daylight"] = not message["state"]["dark"]
bridge_config["sensors"][light_sensor]["state"]["lastupdated"] = message["state"]["lastupdated"]
#Xiaomi motion w/o light level sensor
if message["state"] and bridge_config["deconz"]["sensors"][message["id"]]["modelid"] == "lumi.sensor_motion":
for sensor in bridge_config["sensors"].keys():
if bridge_config["sensors"][sensor]["type"] == "ZLLLightLevel" and bridge_config["sensors"][sensor]["uniqueid"] == bridge_config["sensors"][bridge_sensor_id]["uniqueid"][:-1] + "0":
light_sensor = sensor
break
if bridge_config["sensors"]["1"]["modelid"] == "PHDL00" and bridge_config["sensors"]["1"]["state"]["daylight"]:
bridge_config["sensors"][light_sensor]["state"]["lightlevel"] = 25000
bridge_config["sensors"][light_sensor]["state"]["dark"] = False
else:
bridge_config["sensors"][light_sensor]["state"]["lightlevel"] = 6000
bridge_config["sensors"][light_sensor]["state"]["dark"] = True
#convert xiaomi motion sensor to hue sensor
if message["state"] and bridge_config["deconz"]["sensors"][message["id"]]["modelid"] == "lumi.sensor_motion.aq2" and message["state"] and bridge_config["deconz"]["sensors"][message["id"]]["type"] == "ZHALightLevel":
bridge_config["sensors"][bridge_sensor_id]["state"].update(message["state"])
return
##############
##convert xiaomi vibration sensor states to hue motion sensor
if message["state"] and bridge_config["deconz"]["sensors"][message["id"]]["modelid"] == "lumi.vibration.aq1":
#find the light sensor id
light_sensor = "0"
for sensor in bridge_config["sensors"].keys():
if bridge_config["sensors"][sensor]["type"] == "ZLLLightLevel" and bridge_config["sensors"][sensor]["uniqueid"] == bridge_config["sensors"][bridge_sensor_id]["uniqueid"][:-1] + "0":
light_sensor = sensor
break
logging.info("Vibration: emulated light sensor id is " + light_sensor)
if bridge_config["deconz"]["sensors"][message["id"]]["lightsensor"] == "none":
message["state"].update({"dark": True})
logging.info("Vibration: set light sensor to dark because 'lightsensor' = 'none' ")
elif bridge_config["deconz"]["sensors"][message["id"]]["lightsensor"] == "astral":
message["state"]["dark"] = not bridge_config["sensors"]["1"]["state"]["daylight"]
logging.info("Vibration: set light sensor to " + str(not bridge_config["sensors"]["1"]["state"]["daylight"]) + " because 'lightsensor' = 'astral' ")
if message["state"]["dark"]:
bridge_config["sensors"][light_sensor]["state"]["lightlevel"] = 6000
else:
bridge_config["sensors"][light_sensor]["state"]["lightlevel"] = 25000
bridge_config["sensors"][light_sensor]["state"]["dark"] = message["state"]["dark"]
bridge_config["sensors"][light_sensor]["state"]["daylight"] = not message["state"]["dark"]
bridge_config["sensors"][light_sensor]["state"]["lastupdated"] = message["state"]["lastupdated"]
message["state"] = {"motion": True, "lastupdated": message["state"]["lastupdated"]} #empty the message state for non Hue motion states (we need to know there was an event only)
logging.info("Vibration: set motion = True")
Thread(target=motionDetected, args=[bridge_sensor_id]).start()
bridge_config["sensors"][bridge_sensor_id]["state"].update(message["state"])
current_time = datetime.now()
for key in message["state"].keys():
dxState["sensors"][bridge_sensor_id]["state"][key] = current_time
rulesProcessor(["sensors", bridge_sensor_id], current_time)
if "buttonevent" in message["state"] and bridge_config["deconz"]["sensors"][message["id"]]["modelid"] in ["TRADFRI remote control","RWL021"]:
if message["state"]["buttonevent"] in [1001, 2001, 3001, 4001, 5001]:
Thread(target=longPressButton, args=[bridge_sensor_id, message["state"]["buttonevent"]]).start()
if "presence" in message["state"] and message["state"]["presence"] and "virtual_light" in bridge_config["alarm_config"] and bridge_config["lights"][bridge_config["alarm_config"]["virtual_light"]]["state"]["on"]:
sendEmail(bridge_config["alarm_config"], bridge_config["sensors"][bridge_sensor_id]["name"])
bridge_config["alarm_config"]["virtual_light"]
elif "config" in message and bridge_config["sensors"][bridge_sensor_id]["config"]["on"]:
bridge_config["sensors"][bridge_sensor_id]["config"].update(message["config"])
elif message["r"] == "lights":
bridge_light_id = bridge_config["deconz"]["lights"][message["id"]]["bridgeid"]
if "state" in message and "colormode" not in message["state"]:
bridge_config["lights"][bridge_light_id]["state"].update(message["state"])
updateGroupStats(bridge_light_id, bridge_config["lights"], bridge_config["groups"])
except Exception as e:
logging.info("unable to process the request" + str(e))
try:
ws = EchoClient('ws://' + deconz_ip + ':' + str(bridge_config["deconz"]["websocketport"]))
ws.connect()
ws.run_forever()
except KeyboardInterrupt:
ws.close()
def scanDeconz():
if not bridge_config["deconz"]["enabled"]:
if "username" not in bridge_config["deconz"]:
try:
registration = json.loads(sendRequest("http://" + deconz_ip + ":" + str(bridge_config["deconz"]["port"]) + "/api", "POST", "{\"username\": \"283145a4e198cc6535\", \"devicetype\":\"Hue Emulator\"}"))
except: