├── README.md ├── config.json └── HueBridgeEmulator.py /README.md: -------------------------------------------------------------------------------- 1 | ## HueBridgeEmulator 2 | 3 | This is a lite version of my DiyHue project, i remove all functions that manage wifi lights and sensors. This is intended just to test Hue apps without a real Philips Hue Bridge. 4 | Very usefull to understand how bridge process rules. 5 | Edit config.json to add more lights or sesors. 6 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "UTC": "2017-04-26T20:16:20", 4 | "apiversion": "1.19.0", 5 | "bridgeid": "B827EBC76B26", 6 | "datastoreversion": 59, 7 | "dhcp": true, 8 | "factorynew": false, 9 | "gateway": "192.168.1.1", 10 | "ipaddress": "192.168.10.200", 11 | "linkbutton": true, 12 | "localtime": "2017-04-26T23:16:20", 13 | "mac": "b8:27:eb:c7:6b:26", 14 | "modelid": "BSB002", 15 | "name": "Philips hue", 16 | "netmask": "255.255.255.0", 17 | "portalservices": false, 18 | "proxyaddress": "none", 19 | "proxyport": 0, 20 | "swversion": "1705121051", 21 | "timezone": "Europe/Bucharest", 22 | "whitelist": { 23 | "a7161538be80d40b3de98dece6e91f904dc96170": { 24 | "create date": "2017-05-18T12:21:43", 25 | "last use date": "2017-05-18T12:21:43", 26 | "name": "H" 27 | } 28 | }, 29 | "zigbeechannel": 15 30 | }, 31 | "groups": {}, 32 | "lights": { 33 | "1": { 34 | "modelid": "LCT001", 35 | "name": "Hue rgb bulb 1", 36 | "state": { 37 | "alert": "none", 38 | "bri": 200, 39 | "colormode": "ct", 40 | "ct": 461, 41 | "effect": "none", 42 | "hue": 0, 43 | "on": false, 44 | "reachable": true, 45 | "sat": 0, 46 | "xy": [ 47 | 0.0, 48 | 0.0 49 | ] 50 | }, 51 | "swversion": "66009461", 52 | "type": "Extended color light", 53 | "uniqueid": "45:a3:d7:7f:cf:5c-1" 54 | }, 55 | "2": { 56 | "modelid": "LCT001", 57 | "name": "Hue rgb bulb 2", 58 | "state": { 59 | "alert": "select", 60 | "bri": 254, 61 | "colormode": "ct", 62 | "ct": 156, 63 | "effect": "none", 64 | "hue": 0, 65 | "on": false, 66 | "reachable": true, 67 | "sat": 0, 68 | "xy": [ 69 | 0.0, 70 | 0.0 71 | ] 72 | }, 73 | "swversion": "66009461", 74 | "type": "Extended color light", 75 | "uniqueid": "82:a7:17:a6:20:a0-1" 76 | }, 77 | "3": { 78 | "modelid": "LST001", 79 | "name": "Hue rgb strip 1", 80 | "state": { 81 | "alert": "none", 82 | "bri": 170, 83 | "colormode": "ct", 84 | "ct": 461, 85 | "effect": "none", 86 | "hue": 0, 87 | "on": true, 88 | "reachable": true, 89 | "sat": 0, 90 | "xy": [ 91 | 0.0, 92 | 0.0 93 | ] 94 | }, 95 | "swversion": "66009461", 96 | "type": "Extended color light", 97 | "uniqueid": "ce:9:4:a6:20:a0-1" 98 | }, 99 | "4": { 100 | "modelid": "LST001", 101 | "name": "Hue rgb strip 2", 102 | "state": { 103 | "alert": "none", 104 | "bri": 177, 105 | "colormode": "ct", 106 | "ct": 461, 107 | "effect": "none", 108 | "hue": 0, 109 | "on": true, 110 | "reachable": true, 111 | "sat": 0, 112 | "xy": [ 113 | 0.0, 114 | 0.0 115 | ] 116 | }, 117 | "swversion": "66009461", 118 | "type": "Extended color light", 119 | "uniqueid": "ce:9:4:a6:20:a0-2" 120 | }, 121 | "5": { 122 | "modelid": "LST001", 123 | "name": "Hue rgb strip 3", 124 | "state": { 125 | "alert": "none", 126 | "bri": 177, 127 | "colormode": "ct", 128 | "ct": 461, 129 | "effect": "none", 130 | "hue": 0, 131 | "on": true, 132 | "reachable": true, 133 | "sat": 0, 134 | "xy": [ 135 | 0.0, 136 | 0.0 137 | ] 138 | }, 139 | "swversion": "66009461", 140 | "type": "Extended color light", 141 | "uniqueid": "ce:9:4:a6:20:a0-3" 142 | }, 143 | "6": { 144 | "modelid": "LCT001", 145 | "name": "Hue rgb bulb 3", 146 | "state": { 147 | "alert": "none", 148 | "bri": 124, 149 | "colormode": "ct", 150 | "ct": 461, 151 | "effect": "none", 152 | "hue": 0, 153 | "on": false, 154 | "reachable": true, 155 | "sat": 0, 156 | "xy": [ 157 | 0.0, 158 | 0.0 159 | ] 160 | }, 161 | "swversion": "66009461", 162 | "type": "Extended color light", 163 | "uniqueid": "92:a0:d7:7f:cf:5c-1" 164 | } 165 | }, 166 | "resourcelinks": {}, 167 | "rules": {}, 168 | "scenes": {}, 169 | "schedules": {}, 170 | "sensors": { 171 | "1": { 172 | "config": { 173 | "battery": 100, 174 | "on": true, 175 | "reachable": true 176 | }, 177 | "manufacturername": "Philips", 178 | "modelid": "RWL021", 179 | "name": "Dimmer Switch", 180 | "state": { 181 | "buttonevent": "4000", 182 | "lastupdated": "2017-05-03T23:18:25" 183 | }, 184 | "swversion": "5.45.1.17846", 185 | "type": "ZLLSwitch", 186 | "uniqueid": "a0:20:a6:6:cb:77" 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /HueBridgeEmulator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from __future__ import print_function 4 | 5 | import logging 6 | import os 7 | import signal 8 | import sys 9 | import time 10 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 11 | from time import strftime, sleep 12 | from datetime import datetime, timedelta 13 | from pprint import pprint 14 | from subprocess import check_output 15 | import json, socket, hashlib, urllib2, struct, random 16 | from threading import Thread 17 | from collections import defaultdict 18 | from uuid import getnode as get_mac 19 | from urlparse import urlparse, parse_qs 20 | 21 | mac = '%012x' % get_mac() 22 | 23 | listen_port = 80 # port real hardware uses is regular http port 80 24 | 25 | if os.getenv('HUE_PORT'): 26 | listen_port = int(os.getenv('HUE_PORT')) 27 | 28 | run_service = True 29 | 30 | bridge_config = defaultdict(lambda:defaultdict(str)) 31 | sensors_state = {} 32 | 33 | logger = logging.getLogger('hue') 34 | handler = logging.StreamHandler(sys.stdout) 35 | logger.addHandler(handler) 36 | 37 | handler.setLevel(logging.INFO) 38 | logger.setLevel(logging.INFO) 39 | 40 | #load config files 41 | def get_config_from_file(): 42 | try: 43 | with open('config.json', 'r') as fp: 44 | result = json.load(fp) 45 | logger.info("config loaded") 46 | return result 47 | except Exception: 48 | logger.exception("config file was not loaded") 49 | 50 | 51 | def load_config(): 52 | global bridge_config 53 | bridge_config = get_config_from_file() 54 | 55 | 56 | def generate_sensors_state(): 57 | for sensor in bridge_config["sensors"]: 58 | if sensor not in sensors_state and "state" in bridge_config["sensors"][sensor]: 59 | sensors_state[sensor] = {"state": {}} 60 | for key in bridge_config["sensors"][sensor]["state"].keys(): 61 | if key in ["lastupdated", "presence", "flag", "dark", "status"]: 62 | sensors_state[sensor]["state"].update({key: "2017-01-01T00:00:00"}) 63 | 64 | generate_sensors_state() 65 | 66 | def get_ip_address(): 67 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 68 | s.connect(("8.8.8.8", listen_port)) 69 | return s.getsockname()[0] 70 | 71 | bridge_config["config"]["ipaddress"] = get_ip_address() 72 | 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] 73 | bridge_config["config"]["bridgeid"] = mac.upper() 74 | 75 | def save_config(): 76 | with open('config.json', 'w') as fp: 77 | json.dump(bridge_config, fp, sort_keys=True, indent=4, separators=(',', ': ')) 78 | 79 | def ssdp_search(): 80 | SSDP_ADDR = '239.255.255.250' 81 | SSDP_PORT = 1900 82 | MSEARCH_Interval = 2 83 | multicast_group_c = SSDP_ADDR 84 | multicast_group_s = (SSDP_ADDR, SSDP_PORT) 85 | server_address = ('', SSDP_PORT) 86 | Response_message = 'HTTP/1.1 200 OK\r\nHOST: 239.255.255.250:1900\r\nEXT:\r\nCACHE-CONTROL: max-age=100\r\nLOCATION: http://' + get_ip_address() + ':' + str(listen_port) + '/description.xml\r\nSERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.16.0\r\nhue-bridgeid: ' + mac.upper() + '\r\nST: urn:schemas-upnp-org:device:basic:1\r\nUSN: uuid:2f402f80-da50-11e1-9b23-' + mac 87 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 88 | sock.bind(server_address) 89 | 90 | group = socket.inet_aton(multicast_group_c) 91 | mreq = struct.pack('4sL', group, socket.INADDR_ANY) 92 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) 93 | 94 | print("starting ssdp...") 95 | 96 | while run_service: 97 | data, address = sock.recvfrom(1024) 98 | if data[0:19]== 'M-SEARCH * HTTP/1.1': 99 | if data.find("ssdp:all") != -1: 100 | sleep(random.randrange(0, 3)) 101 | print("Sending M Search response") 102 | sock.sendto(Response_message, address) 103 | sleep(1) 104 | sock.close() 105 | 106 | 107 | def scheduler_processor(): 108 | while run_service: 109 | try: 110 | process_rules() 111 | except Exception: 112 | logger.exception("Error during processing rules.") 113 | rules_processor(True) 114 | sleep(1) 115 | 116 | 117 | def process_rules(): 118 | for schedule in bridge_config["schedules"].keys(): 119 | if bridge_config["schedules"][schedule]["status"] == "enabled": 120 | if bridge_config["schedules"][schedule]["localtime"].startswith("W"): 121 | pices = bridge_config["schedules"][schedule]["localtime"].split('/T') 122 | if int(pices[0][1:]) & (1 << 6 - datetime.today().weekday()): 123 | if pices[1] == datetime.now().strftime("%H:%M:%S"): 124 | print("execute schedule: " + schedule) 125 | sendRequest(bridge_config["schedules"][schedule]["command"]["address"], 126 | bridge_config["schedules"][schedule]["command"]["method"], 127 | json.dumps(bridge_config["schedules"][schedule]["command"]["body"])) 128 | elif bridge_config["schedules"][schedule]["localtime"].startswith("PT"): 129 | if bridge_config["schedules"][schedule]["starttime"] == datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"): 130 | print("execute timmer: " + schedule) 131 | sendRequest(bridge_config["schedules"][schedule]["command"]["address"], 132 | bridge_config["schedules"][schedule]["command"]["method"], 133 | json.dumps(bridge_config["schedules"][schedule]["command"]["body"])) 134 | bridge_config["schedules"][schedule]["status"] = "disabled" 135 | else: 136 | if bridge_config["schedules"][schedule]["localtime"] == datetime.now().strftime("%Y-%m-%dT%H:%M:%S"): 137 | print("execute schedule: " + schedule) 138 | sendRequest(bridge_config["schedules"][schedule]["command"]["address"], 139 | bridge_config["schedules"][schedule]["command"]["method"], 140 | json.dumps(bridge_config["schedules"][schedule]["command"]["body"])) 141 | if (datetime.now().strftime("%M:%S") == "00:00"): # auto save configuration every hour 142 | save_config() 143 | 144 | 145 | def rules_processor(scheduler=False): 146 | bridge_config["config"]["localtime"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") #required for operator dx to address /config/localtime 147 | for rule in bridge_config["rules"].keys(): 148 | if bridge_config["rules"][rule]["status"] == "enabled": 149 | execute = True 150 | for condition in bridge_config["rules"][rule]["conditions"]: 151 | url_pices = condition["address"].split('/') 152 | if condition["operator"] == "eq": 153 | if condition["value"] == "true": 154 | if not bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]: 155 | execute = False 156 | elif condition["value"] == "false": 157 | if bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]: 158 | execute = False 159 | else: 160 | if not int(bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]) == int(condition["value"]): 161 | execute = False 162 | elif condition["operator"] == "gt": 163 | if not int(bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]) > int(condition["value"]): 164 | execute = False 165 | elif condition["operator"] == "lt": 166 | if int(not bridge_config[url_pices[1]][url_pices[2]][url_pices[3]][url_pices[4]]) < int(condition["value"]): 167 | execute = False 168 | elif condition["operator"] == "dx": 169 | if not sensors_state[url_pices[2]][url_pices[3]][url_pices[4]] == datetime.now().strftime("%Y-%m-%dT%H:%M:%S"): 170 | execute = False 171 | elif condition["operator"] == "ddx": 172 | if not scheduler: 173 | execute = False 174 | else: 175 | ddx_time = datetime.strptime(condition["value"],"PT%H:%M:%S") 176 | if not (datetime.strptime(sensors_state[url_pices[2]][url_pices[3]][url_pices[4]],"%Y-%m-%dT%H:%M:%S") + timedelta(hours=ddx_time.hour, minutes=ddx_time.minute, seconds=ddx_time.second)) == datetime.now().replace(microsecond=0): 177 | execute = False 178 | elif condition["operator"] == "in": 179 | periods = condition["value"].split('/') 180 | if condition["value"][0] == "T": 181 | timeStart = datetime.strptime(periods[0], "T%H:%M:%S").time() 182 | timeEnd = datetime.strptime(periods[1], "T%H:%M:%S").time() 183 | now_time = datetime.now().time() 184 | if timeStart < timeEnd: 185 | if not timeStart <= now_time <= timeEnd: 186 | execute = False 187 | else: 188 | if not (timeStart <= now_time or now_time <= timeEnd): 189 | execute = False 190 | 191 | if execute: 192 | print("rule " + rule + " is triggered") 193 | for action in bridge_config["rules"][rule]["actions"]: 194 | Thread(target=sendRequest, args=["/api/" + bridge_config["rules"][rule]["owner"] + action["address"], action["method"], json.dumps(action["body"])]).start() 195 | 196 | def sendRequest(url, method, data, time_out=3): 197 | if not url.startswith( 'http://' ): 198 | url = "http://127.0.0.1" + url 199 | opener = urllib2.build_opener(urllib2.HTTPHandler) 200 | request = urllib2.Request(url, data=data) 201 | request.add_header("Content-Type",'application/json') 202 | request.get_method = lambda: method 203 | response = opener.open(request, timeout=time_out).read() 204 | return response 205 | 206 | 207 | def sendLightRequest(light, data): 208 | print("Update light " + light + " with " + json.dumps(data)) 209 | 210 | 211 | def update_group_stats(light): #set group stats based on lights status in that group 212 | for group in bridge_config["groups"]: 213 | if light in bridge_config["groups"][group]["lights"]: 214 | for key, value in bridge_config["lights"][light]["state"].items(): 215 | if key not in ["on", "reachable"]: 216 | bridge_config["groups"][group]["action"][key] = value 217 | any_on = False 218 | all_on = True 219 | bri = 0 220 | for group_light in bridge_config["groups"][group]["lights"]: 221 | if bridge_config["lights"][light]["state"]["on"] == True: 222 | any_on = True 223 | else: 224 | all_on = False 225 | bri += bridge_config["lights"][light]["state"]["bri"] 226 | avg_bri = bri / len(bridge_config["groups"][group]["lights"]) 227 | bridge_config["groups"][group]["state"] = {"any_on": any_on, "all_on": all_on, "bri": avg_bri, "lastupdated": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")} 228 | 229 | 230 | def scan_for_lights(): #scan for ESP8266 lights and strips 231 | print(json.dumps([{"success": {"/lights": "Searching for new devices"}}], sort_keys=True, indent=4, separators=(',', ': '))) 232 | 233 | 234 | def description(): 235 | return """ 236 | 237 | 1 238 | 0 239 | 240 | http://""" + get_ip_address() + """:""" + str(listen_port) + """/ 241 | 242 | urn:schemas-upnp-org:device:Basic:1 243 | Philips hue 244 | Royal Philips Electronics 245 | http://www.philips.com 246 | Philips hue Personal Wireless Lighting 247 | Philips hue bridge 2015 248 | BSB002 249 | http://www.meethue.com 250 | """ + mac.upper() + """ 251 | MYUUID 252 | index.html 253 | 254 | 255 | image/png 256 | 48 257 | 48 258 | 24 259 | hue_logo_0.png 260 | 261 | 262 | image/png 263 | 120 264 | 120 265 | 24 266 | hue_logo_3.png 267 | 268 | 269 | 270 | """ 271 | 272 | class S(BaseHTTPRequestHandler): 273 | def _set_headers(self): 274 | self.send_response(200) 275 | self.send_header('Content-type', 'text/html') 276 | self.end_headers() 277 | 278 | def do_GET(self): 279 | self._set_headers() 280 | if self.path == '/description.xml': 281 | self.wfile.write(description()) 282 | else: 283 | url_pices = self.path.split('/') 284 | if url_pices[2] in bridge_config["config"]["whitelist"]: #if username is in whitelist 285 | bridge_config["config"]["UTC"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") 286 | bridge_config["config"]["localtime"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") 287 | if len(url_pices) == 3: #print entire config 288 | self.wfile.write(json.dumps(bridge_config)) 289 | elif len(url_pices) == 4: #print specified object config 290 | self.wfile.write(json.dumps(bridge_config[url_pices[3]])) 291 | elif len(url_pices) == 5: 292 | if url_pices[4] == "new": #return new lights and sensors only 293 | self.wfile.write(json.dumps({"lastscan": datetime.now().strftime("%Y-%m-%dT%H:%M:%S")})) 294 | else: 295 | self.wfile.write(json.dumps(bridge_config[url_pices[3]][url_pices[4]])) 296 | elif len(url_pices) == 6: 297 | self.wfile.write(json.dumps(bridge_config[url_pices[3]][url_pices[4]][url_pices[5]])) 298 | elif (url_pices[2] == "nouser" or url_pices[2] == "config"): #used by applications to discover the bridge 299 | self.wfile.write(json.dumps({"name": bridge_config["config"]["name"],"datastoreversion": 59, "swversion": bridge_config["config"]["swversion"], "apiversion": bridge_config["config"]["apiversion"], "mac": bridge_config["config"]["mac"], "bridgeid": bridge_config["config"]["bridgeid"], "factorynew": False, "modelid": bridge_config["config"]["modelid"]})) 300 | else: #user is not in whitelist 301 | self.wfile.write(json.dumps([{"error": {"type": 1, "address": self.path, "description": "unauthorized user" }}])) 302 | 303 | 304 | def do_POST(self): 305 | self._set_headers() 306 | print("in post method") 307 | self.data_string = self.rfile.read(int(self.headers['Content-Length'])) 308 | post_dictionary = json.loads(self.data_string) 309 | url_pices = self.path.split('/') 310 | print(self.path) 311 | print(self.data_string) 312 | if len(url_pices) == 4: #data was posted to a location 313 | if url_pices[2] in bridge_config["config"]["whitelist"]: 314 | if ((url_pices[3] == "lights" or url_pices[3] == "sensors") and not bool(post_dictionary)): 315 | #if was a request to scan for lights of sensors 316 | Thread(target=scan_for_lights).start() 317 | sleep(7) #give no more than 7 seconds for light scanning (otherwise will face app disconnection timeout) 318 | self.wfile.write(json.dumps([{"success": {"/" + url_pices[3]: "Searching for new devices"}}])) 319 | else: #create object 320 | # find the first unused id for new object 321 | i = 1 322 | while (str(i)) in bridge_config[url_pices[3]]: 323 | i += 1 324 | if url_pices[3] == "scenes": 325 | post_dictionary.update({"lightstates": {}, "version": 2, "picture": "", "lastupdated": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")}) 326 | elif url_pices[3] == "groups": 327 | post_dictionary.update({"action": {"on": False}, "state": {"any_on": False, "all_on": False}}) 328 | elif url_pices[3] == "schedules": 329 | post_dictionary.update({"created": datetime.now().strftime("%Y-%m-%dT%H:%M:%S")}) 330 | if post_dictionary["localtime"].startswith("PT"): 331 | timmer = post_dictionary["localtime"][2:] 332 | (h, m, s) = timmer.split(':') 333 | d = timedelta(hours=int(h), minutes=int(m), seconds=int(s)) 334 | post_dictionary.update({"starttime": (datetime.utcnow() + d).strftime("%Y-%m-%dT%H:%M:%S")}) 335 | if not "status" in post_dictionary: 336 | post_dictionary.update({"status": "enabled"}) 337 | elif url_pices[3] == "rules": 338 | post_dictionary.update({"owner": url_pices[2]}) 339 | if not "status" in post_dictionary: 340 | post_dictionary.update({"status": "enabled"}) 341 | elif url_pices[3] == "sensors": 342 | if post_dictionary["modelid"] == "PHWA01": 343 | post_dictionary.update({"state": {"status": 0}}) 344 | generate_sensors_state() 345 | bridge_config[url_pices[3]][str(i)] = post_dictionary 346 | print(json.dumps([{"success": {"id": str(i)}}], sort_keys=True, indent=4, separators=(',', ': '))) 347 | self.wfile.write(json.dumps([{"success": {"id": str(i)}}], sort_keys=True, indent=4, separators=(',', ': '))) 348 | else: 349 | self.wfile.write(json.dumps([{"error": {"type": 1, "address": self.path, "description": "unauthorized user" }}],sort_keys=True, indent=4, separators=(',', ': '))) 350 | print(json.dumps([{"error": {"type": 1, "address": self.path, "description": "unauthorized user" }}],sort_keys=True, indent=4, separators=(',', ': '))) 351 | elif "devicetype" in post_dictionary: #this must be a new device registration 352 | #create new user hash 353 | s = hashlib.new('ripemd160', post_dictionary["devicetype"][0] ).digest() 354 | username = s.encode('hex') 355 | bridge_config["config"]["whitelist"][username] = {"last use date": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"),"create date": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"),"name": post_dictionary["devicetype"]} 356 | self.wfile.write(json.dumps([{"success": {"username": username}}], sort_keys=True, indent=4, separators=(',', ': '))) 357 | print(json.dumps([{"success": {"username": username}}], sort_keys=True, indent=4, separators=(',', ': '))) 358 | self.end_headers() 359 | save_config() 360 | 361 | def do_PUT(self): 362 | self._set_headers() 363 | print("in PUT method") 364 | self.data_string = self.rfile.read(int(self.headers['Content-Length'])) 365 | put_dictionary = json.loads(self.data_string) 366 | url_pices = self.path.split('/') 367 | if url_pices[2] in bridge_config["config"]["whitelist"]: 368 | if len(url_pices) == 4: 369 | bridge_config[url_pices[3]].update(put_dictionary) 370 | response_location = "/" + url_pices[3] + "/" 371 | if len(url_pices) == 5: 372 | if url_pices[3] == "schedules": 373 | if "status" in put_dictionary and put_dictionary["status"] == "enabled" and bridge_config["schedules"][url_pices[4]]["localtime"].startswith("PT"): 374 | if "localtime" in put_dictionary: 375 | timmer = put_dictionary["localtime"][2:] 376 | else: 377 | timmer = bridge_config["schedules"][url_pices[4]]["localtime"][2:] 378 | (h, m, s) = timmer.split(':') 379 | d = timedelta(hours=int(h), minutes=int(m), seconds=int(s)) 380 | put_dictionary.update({"starttime": (datetime.utcnow() + d).strftime("%Y-%m-%dT%H:%M:%S")}) 381 | elif url_pices[3] == "scenes": 382 | if "storelightstate" in put_dictionary: 383 | for light in bridge_config["scenes"][url_pices[4]]["lightstates"]: 384 | bridge_config["scenes"][url_pices[4]]["lightstates"][light]["on"] = bridge_config["lights"][light]["state"]["on"] 385 | bridge_config["scenes"][url_pices[4]]["lightstates"][light]["bri"] = bridge_config["lights"][light]["state"]["bri"] 386 | if "xy" in bridge_config["scenes"][url_pices[4]]["lightstates"][light]: 387 | del bridge_config["scenes"][url_pices[4]]["lightstates"][light]["xy"] 388 | elif "ct" in bridge_config["scenes"][url_pices[4]]["lightstates"][light]: 389 | del bridge_config["scenes"][url_pices[4]]["lightstates"][light]["ct"] 390 | elif "hue" in bridge_config["scenes"][url_pices[4]]["lightstates"][light]: 391 | del bridge_config["scenes"][url_pices[4]]["lightstates"][light]["hue"] 392 | del bridge_config["scenes"][url_pices[4]]["lightstates"][light]["sat"] 393 | if bridge_config["lights"][light]["state"]["colormode"] in ["ct", "xy"]: 394 | bridge_config["scenes"][url_pices[4]]["lightstates"][light][bridge_config["lights"][light]["state"]["colormode"]] = bridge_config["lights"][light]["state"][bridge_config["lights"][light]["state"]["colormode"]] 395 | elif bridge_config["lights"][light]["state"]["colormode"] == "hs": 396 | bridge_config["scenes"][url_pices[4]]["lightstates"][light]["hue"] = bridge_config["lights"][light]["state"]["hue"] 397 | bridge_config["scenes"][url_pices[4]]["lightstates"][light]["sat"] = bridge_config["lights"][light]["state"]["sat"] 398 | 399 | if url_pices[3] == "sensors": 400 | for key, value in put_dictionary.items(): 401 | bridge_config[url_pices[3]][url_pices[4]][key].update(value) 402 | else: 403 | bridge_config[url_pices[3]][url_pices[4]].update(put_dictionary) 404 | response_location = "/" + url_pices[3] + "/" + url_pices[4] + "/" 405 | if len(url_pices) == 6: 406 | if url_pices[3] == "groups": #state is applied to a group 407 | if "scene" in put_dictionary: #if group is 0 and there is a scene applied 408 | for light in bridge_config["scenes"][put_dictionary["scene"]]["lights"]: 409 | bridge_config["lights"][light]["state"].update(bridge_config["scenes"][put_dictionary["scene"]]["lightstates"][light]) 410 | if "xy" in bridge_config["scenes"][put_dictionary["scene"]]["lightstates"][light]: 411 | bridge_config["lights"][light]["state"]["colormode"] = "xy" 412 | elif "ct" in bridge_config["scenes"][put_dictionary["scene"]]["lightstates"][light]: 413 | bridge_config["lights"][light]["state"]["colormode"] = "ct" 414 | elif "hue" or "sat" in bridge_config["scenes"][put_dictionary["scene"]]["lightstates"][light]: 415 | bridge_config["lights"][light]["state"]["colormode"] = "hs" 416 | Thread(target=sendLightRequest, args=[light, bridge_config["scenes"][put_dictionary["scene"]]["lightstates"][light]]).start() 417 | update_group_stats(light) 418 | elif "bri_inc" in put_dictionary: 419 | bridge_config["groups"][url_pices[4]]["action"]["bri"] += int(put_dictionary["bri_inc"]) 420 | if bridge_config["groups"][url_pices[4]]["action"]["bri"] > 254: 421 | bridge_config["groups"][url_pices[4]]["action"]["bri"] = 254 422 | elif bridge_config["groups"][url_pices[4]]["action"]["bri"] < 1: 423 | bridge_config["groups"][url_pices[4]]["action"]["bri"] = 1 424 | bridge_config["groups"][url_pices[4]]["state"]["bri"] = bridge_config["groups"][url_pices[4]]["action"]["bri"] 425 | del put_dictionary["bri_inc"] 426 | put_dictionary.update({"bri": bridge_config["groups"][url_pices[4]]["action"]["bri"]}) 427 | for light in bridge_config["groups"][url_pices[4]]["lights"]: 428 | bridge_config["lights"][light]["state"].update(put_dictionary) 429 | Thread(target=sendLightRequest, args=[light, put_dictionary]).start() 430 | elif url_pices[4] == "0": 431 | for light in bridge_config["lights"].keys(): 432 | bridge_config["lights"][light]["state"].update(put_dictionary) 433 | Thread(target=sendLightRequest, args=[light, put_dictionary]).start() 434 | for group in bridge_config["groups"].keys(): 435 | bridge_config["groups"][group][url_pices[5]].update(put_dictionary) 436 | if "on" in put_dictionary: 437 | bridge_config["groups"][group]["state"]["any_on"] = put_dictionary["on"] 438 | bridge_config["groups"][group]["state"]["all_on"] = put_dictionary["on"] 439 | else: # the state is applied to particular group (url_pices[4]) 440 | if "on" in put_dictionary: 441 | bridge_config["groups"][url_pices[4]]["state"]["any_on"] = put_dictionary["on"] 442 | bridge_config["groups"][url_pices[4]]["state"]["all_on"] = put_dictionary["on"] 443 | for light in bridge_config["groups"][url_pices[4]]["lights"]: 444 | bridge_config["lights"][light]["state"].update(put_dictionary) 445 | Thread(target=sendLightRequest, args=[light, put_dictionary]).start() 446 | elif url_pices[3] == "lights": #state is applied to a light 447 | Thread(target=sendLightRequest, args=[url_pices[4], put_dictionary]).start() 448 | for key in put_dictionary.keys(): 449 | if key in ["ct", "xy"]: #colormode must be set by bridge 450 | bridge_config["lights"][url_pices[4]]["state"]["colormode"] = key 451 | elif key in ["hue", "sat"]: 452 | bridge_config["lights"][url_pices[4]]["state"]["colormode"] = "hs" 453 | update_group_stats(url_pices[4]) 454 | if not url_pices[4] == "0": #group 0 is virtual, must not be saved in bridge configuration 455 | try: 456 | bridge_config[url_pices[3]][url_pices[4]][url_pices[5]].update(put_dictionary) 457 | except KeyError: 458 | bridge_config[url_pices[3]][url_pices[4]][url_pices[5]] = put_dictionary 459 | if url_pices[3] == "sensors" and url_pices[5] == "state": 460 | for key in put_dictionary.keys(): 461 | sensors_state[url_pices[4]]["state"].update({key: datetime.now().strftime("%Y-%m-%dT%H:%M:%S")}) 462 | if "flag" in put_dictionary: #if a scheduler change te flag of a logical sensor then process the rules. 463 | rules_processor() 464 | response_location = "/" + url_pices[3] + "/" + url_pices[4] + "/" + url_pices[5] + "/" 465 | if len(url_pices) == 7: 466 | try: 467 | bridge_config[url_pices[3]][url_pices[4]][url_pices[5]][url_pices[6]].update(put_dictionary) 468 | except KeyError: 469 | bridge_config[url_pices[3]][url_pices[4]][url_pices[5]][url_pices[6]] = put_dictionary 470 | bridge_config[url_pices[3]][url_pices[4]][url_pices[5]][url_pices[6]] = put_dictionary 471 | response_location = "/" + url_pices[3] + "/" + url_pices[4] + "/" + url_pices[5] + "/" + url_pices[6] + "/" 472 | response_dictionary = [] 473 | for key, value in put_dictionary.items(): 474 | response_dictionary.append({"success":{response_location + key: value}}) 475 | self.wfile.write(json.dumps(response_dictionary,sort_keys=True, indent=4, separators=(',', ': '))) 476 | print(json.dumps(response_dictionary, sort_keys=True, indent=4, separators=(',', ': '))) 477 | else: 478 | self.wfile.write(json.dumps([{"error": {"type": 1, "address": self.path, "description": "unauthorized user" }}],sort_keys=True, indent=4, separators=(',', ': '))) 479 | 480 | def do_DELETE(self): 481 | self._set_headers() 482 | url_pices = self.path.split('/') 483 | if url_pices[2] in bridge_config["config"]["whitelist"]: 484 | del bridge_config[url_pices[3]][url_pices[4]] 485 | self.wfile.write(json.dumps([{"success": "/" + url_pices[3] + "/" + url_pices[4] + " deleted."}])) 486 | 487 | 488 | class Application(object): 489 | 490 | def __init__(self): 491 | self.httpd = None 492 | """:type:BaseHTTPServer.HTTPServer | None""" 493 | self.shutdown_req = False 494 | self.running = True 495 | 496 | def shutdown_executor(self): 497 | while self.running: 498 | if self.shutdown_req: 499 | time.sleep(1) 500 | self.httpd.shutdown() 501 | 502 | def run(self, server_class=HTTPServer, handler_class=S): 503 | signal.signal(signal.SIGTERM, self.exit_handler) 504 | signal.signal(signal.SIGINT, self.exit_handler) 505 | try: 506 | signal.signal(signal.SIGUSR1, self.reload_config_handler) 507 | except Exception: 508 | pass # probably it is Windows 509 | server_address = ('', listen_port) 510 | self.httpd = server_class(server_address, handler_class) 511 | print('Starting httpd on %d...' % listen_port) 512 | self.start_shutdown_executor() 513 | while run_service: 514 | # single request (improper uri) can cause exception and then it results with server is shutting down 515 | # we should start new server to continue handling requests 516 | try: 517 | self.httpd.serve_forever(poll_interval=0.5) 518 | except Exception: 519 | logger.exception("Exception during handling request") 520 | self.running = False 521 | 522 | def start_shutdown_executor(self): 523 | t = Thread(target=self.shutdown_executor) 524 | t.setDaemon(True) 525 | t.start() 526 | 527 | def exit_handler(self, *args): 528 | logger.error("Signal received. Stopping service!") 529 | global run_service 530 | run_service = False 531 | if self.httpd: 532 | logger.info('Stopping server...') 533 | # it is not possible to shutdown http server in interrupt handler as Event.wait inside shutdown() 534 | # would stuck 535 | self.shutdown_req = True 536 | 537 | def reload_config_handler(self, *args): 538 | logger.info("Reloading config") 539 | load_config() 540 | 541 | 542 | if __name__ == "__main__": 543 | try: 544 | app = Application() 545 | load_config() 546 | logger.info("Current config: \n%s", json.dumps(bridge_config, indent=2)) 547 | if os.getenv('RUN_SSDP') != 'n': 548 | t = Thread(target=ssdp_search) 549 | t.setDaemon(True) 550 | t.start() 551 | if os.getenv('RUN_RULES') != 'n': 552 | t = Thread(target=scheduler_processor) 553 | t.setDaemon(True) 554 | t.start() 555 | try: 556 | app.run() 557 | logger.info('Http Server stopped.') 558 | except Exception: 559 | logger.exception("Error during processing request") 560 | 561 | except Exception: 562 | logger.exception("server stopped") 563 | finally: 564 | run_service = False 565 | save_config() 566 | print('config saved') 567 | --------------------------------------------------------------------------------