diff --git a/BridgeEmulator/HueEmulator.py b/BridgeEmulator/HueEmulator.py index d7f0526b..f97c26b1 100755 --- a/BridgeEmulator/HueEmulator.py +++ b/BridgeEmulator/HueEmulator.py @@ -15,24 +15,21 @@ run_service = True bridge_config = defaultdict(lambda:defaultdict(str)) -lights_address = {} new_lights = {} -alarm_config = {} sensors_state = {} -capabilities = {"groups": {"available": 64},"lights": {"available": 63},"resourcelinks": {"available": 64},"rules": {"actions": {"available": 400},"available": 200,"conditions": {"available": 400}},"scenes": {"available": 200,"lightstates": {"available": 2048}},"schedules": {"available": 100},"sensors": {"available": 63,"clip": {"available": 63},"zgp": {"available": 63},"zll": {"available": 63}}} -def send_email(triggered_sensor): +def sendEmail(triggered_sensor): import smtplib TEXT = "Sensor " + triggered_sensor + " was triggered while the alarm is active" # Prepare actual message message = """From: %s\nTo: %s\nSubject: %s\n\n%s - """ % (alarm_config["mail_from"], ", ".join(alarm_config["mail_recipients"]), alarm_config["mail_subject"], TEXT) + """ % (bridge_config["alarm_config"]["mail_from"], ", ".join(bridge_config["alarm_config"]["mail_recipients"]), bridge_config["alarm_config"]["mail_subject"], TEXT) try: - server_ssl = smtplib.SMTP_SSL(alarm_config["smtp_server"], alarm_config["smtp_port"]) + server_ssl = smtplib.SMTP_SSL(bridge_config["alarm_config"]["smtp_server"], bridge_config["alarm_config"]["smtp_port"]) server_ssl.ehlo() # optional, called by login() - server_ssl.login(alarm_config["mail_username"], alarm_config["mail_password"]) - server_ssl.sendmail(alarm_config["mail_from"], alarm_config["mail_recipients"], message) + server_ssl.login(bridge_config["alarm_config"]["mail_username"], bridge_config["alarm_config"]["mail_password"]) + server_ssl.sendmail(bridge_config["alarm_config"]["mail_from"], bridge_config["alarm_config"]["mail_recipients"], message) server_ssl.close() print("successfully sent the mail") return True @@ -49,40 +46,22 @@ def send_email(triggered_sensor): print("CRITICAL! Config file was not loaded") sys.exit(1) -try: - with open('lights_address.json', 'r') as fp: - lights_address = json.load(fp) - print("Lights address loaded") -except Exception: - print("Lights adress file was not loaded") #load and configure alarm virtual light -try: - with open('alarm_config.json', 'r') as fp: - alarm_config = json.load(fp) - print("Alarm config loaded") - if alarm_config["mail_username"] != "": - print("E-mail account configured") - if "virtual_light" not in alarm_config: - print("Send test email") - if send_email("dummy test"): - print("Mail succesfully sent\nCreate alarm virtual light") - i = 1 - while (str(i)) in bridge_config["lights"]: - i += 1 - bridge_config["lights"][str(i)] = {"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"} - alarm_config["virtual_light"] = str(i) - with open('alarm_config.json', 'w') as fp: - json.dump(alarm_config, fp, sort_keys=True, indent=4, separators=(',', ': ')) - else: - print("Mail test failed") +if bridge_config["alarm_config"]["mail_username"] != "": + print("E-mail account configured") + if "virtual_light" not in bridge_config["alarm_config"]: + print("Send test email") + if sendEmail("dummy test"): + print("Mail succesfully sent\nCreate alarm virtual light") + new_light_id = nextFreeId("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: - print("E-mail account not configured") + print("Mail test failed") -except Exception: - print("Alarm config file was not loaded") -def generate_sensors_state(): +def generateSensorsState(): for sensor in bridge_config["sensors"]: if sensor not in sensors_state and "state" in bridge_config["sensors"][sensor]: sensors_state[sensor] = {"state": {}} @@ -90,33 +69,37 @@ def generate_sensors_state(): if key in ["lastupdated", "presence", "flag", "dark", "status"]: sensors_state[sensor]["state"].update({key: "2017-01-01T00:00:00"}) -generate_sensors_state() #comment this line if you don't want to restore last known state to all lights on startup +def nextFreeId(element): + i = 1 + while (str(i)) in bridge_config[element]: + i += 1 + return str(i) + +generateSensorsState() #comment this line if you don't want to restore last known state to all lights on startup -def get_ip_address(): +def getIpAddress(): s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect(("8.8.8.8", 80)) return s.getsockname()[0] -bridge_config["config"]["ipaddress"] = get_ip_address() -bridge_config["config"]["gateway"] = get_ip_address() +bridge_config["config"]["ipaddress"] = getIpAddress() +bridge_config["config"]["gateway"] = getIpAddress() 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() -def save_config(): +def saveConfig(): with open('config.json', 'w') as fp: json.dump(bridge_config, fp, sort_keys=True, indent=4, separators=(',', ': ')) - with open('lights_address.json', 'w') as fp: - json.dump(lights_address, fp, sort_keys=True, indent=4, separators=(',', ': ')) -def ssdp_search(): +def ssdpSearch(): SSDP_ADDR = '239.255.255.250' SSDP_PORT = 1900 MSEARCH_Interval = 2 multicast_group_c = SSDP_ADDR multicast_group_s = (SSDP_ADDR, SSDP_PORT) server_address = ('', SSDP_PORT) - 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() + ':80/description.xml\r\nSERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.20.0\r\nhue-bridgeid: ' + (mac[:6] + 'FFFE' + mac[6:]).upper() + '\r\n' + 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://' + getIpAddress() + ':80/description.xml\r\nSERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.20.0\r\nhue-bridgeid: ' + (mac[:6] + 'FFFE' + mac[6:]).upper() + '\r\n' custom_response_message = {0: {"st": "upnp:rootdevice", "usn": "uuid:2f402f80-da50-11e1-9b23-" + mac + "::upnp:rootdevice"}, 1: {"st": "uuid:2f402f80-da50-11e1-9b23-" + mac, "usn": "uuid:2f402f80-da50-11e1-9b23-" + mac}, 2: {"st": "urn:schemas-upnp-org:device:basic:1", "usn": "uuid:2f402f80-da50-11e1-9b23-" + mac}} sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(server_address) @@ -138,13 +121,13 @@ def ssdp_search(): print(Response_message + "ST: " + custom_response_message[x]["st"] + "\r\nUSN: " + custom_response_message[x]["usn"] + "\r\n\r\n") sleep(1) -def ssdp_broadcast(): +def ssdpBroadcast(): print("start ssdp broadcast") SSDP_ADDR = '239.255.255.250' SSDP_PORT = 1900 MSEARCH_Interval = 2 multicast_group_s = (SSDP_ADDR, SSDP_PORT) - message = 'NOTIFY * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nCACHE-CONTROL: max-age=100\r\nLOCATION: http://' + get_ip_address() + ':80/description.xml\r\nSERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.20.0\r\nNTS: ssdp:alive\r\nhue-bridgeid: ' + (mac[:6] + 'FFFE' + mac[6:]).upper() + '\r\n' + message = 'NOTIFY * HTTP/1.1\r\nHOST: 239.255.255.250:1900\r\nCACHE-CONTROL: max-age=100\r\nLOCATION: http://' + getIpAddress() + ':80/description.xml\r\nSERVER: Linux/3.14.0 UPnP/1.0 IpBridge/1.20.0\r\nNTS: ssdp:alive\r\nhue-bridgeid: ' + (mac[:6] + 'FFFE' + mac[6:]).upper() + '\r\n' custom_message = {0: {"nt": "upnp:rootdevice", "usn": "uuid:2f402f80-da50-11e1-9b23-" + mac + "::upnp:rootdevice"}, 1: {"nt": "uuid:2f402f80-da50-11e1-9b23-" + mac, "usn": "uuid:2f402f80-da50-11e1-9b23-" + mac}, 2: {"nt": "urn:schemas-upnp-org:device:basic:1", "usn": "uuid:2f402f80-da50-11e1-9b23-" + mac}} sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.settimeout(MSEARCH_Interval+0.5) @@ -157,7 +140,7 @@ def ssdp_broadcast(): #print (message + "NT: " + custom_message[x]["nt"] + "\r\nUSN: " + custom_message[x]["usn"] + "\r\n\r\n") sleep(60) -def scheduler_processor(): +def schedulerProcessor(): while run_service: for schedule in bridge_config["schedules"].iterkeys(): delay = 0 @@ -188,10 +171,28 @@ def scheduler_processor(): if bridge_config["schedules"][schedule]["autodelete"]: del bridge_config["schedules"][schedule] if (datetime.now().strftime("%M:%S") == "00:00"): #auto save configuration every hour - save_config() + saveConfig() sleep(1) -def check_rule_conditions(rule, sensor, ignore_ddx=False): +def addTradfriRemote(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 + "/action/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 + "/action/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("resourcelinks") + bridge_config["resourcelinks"][resourcelinkId] = {"classid": 15555,"description": "Rules for sensor " + sensor_id, "links": ["/sensors/3"],"name": "Emulator rules " + sensor_id,"owner": bridge_config["config"]["whitelist"].keys()[0]} + for rule in rules: + ruleId = nextFreeId("rules") + bridge_config["rules"][ruleId] = rule + bridge_config["rules"][ruleId].update({"creationtime": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"), "lasttriggered": None, "owner": bridge_config["config"]["whitelist"].keys()[0], "recycle": True, "status": "enabled", "timestriggered": 0}) + bridge_config["resourcelinks"][resourcelinkId]["links"].append("/rules/" + ruleId); + +def addHueMotionSensor(): + new_sensor_id = nextFreeId("sensors") + bridge_config["sensors"][new_sensor_id] = {"name": "Hue temperature sensor 1", "uniqueid": new_sensor_id + "0f:12:23:34:45:56: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"} + bridge_config["sensors"][str(int(new_sensor_id) + 1)] = {"name": "Entrance Lights sensor", "uniqueid": new_sensor_id + "0f:12:23:34:45:56: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"][str(int(new_sensor_id) + 2)] = {"name": "Hue ambient light sensor 1", "uniqueid": new_sensor_id + "0f:12:23:34:45:56: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(str(int(new_sensor_id) + 1)) + + +def checkRuleConditions(rule, sensor, ignore_ddx=False): ddx = 0 sensor_found = False for condition in bridge_config["rules"][rule]["conditions"]: @@ -239,9 +240,9 @@ def check_rule_conditions(rule, sensor, ignore_ddx=False): else: return [False, ddx] -def ddx_recheck(rule, sensor, ddx_delay): +def ddxRecheck(rule, sensor, ddx_delay): sleep(ddx_delay) - rule_state = check_rule_conditions(rule, sensor, True) + rule_state = checkRuleConditions(rule, sensor, True) if rule_state[0]: #if all conditions are meet again print("delayed rule " + rule + " is triggered") bridge_config["rules"][rule]["lasttriggered"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") @@ -249,11 +250,11 @@ def ddx_recheck(rule, sensor, ddx_delay): for action in bridge_config["rules"][rule]["actions"]: Thread(target=sendRequest, args=["/api/" + bridge_config["rules"][rule]["owner"] + action["address"], action["method"], json.dumps(action["body"])]).start() -def rules_processor(sensor): +def rulesProcessor(sensor): bridge_config["config"]["localtime"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") #required for operator dx to address /config/localtime for rule in bridge_config["rules"].iterkeys(): if bridge_config["rules"][rule]["status"] == "enabled": - rule_result = check_rule_conditions(rule, sensor) + rule_result = checkRuleConditions(rule, sensor) if rule_result[0] and bridge_config["rules"][rule]["lasttriggered"] != datetime.now().strftime("%Y-%m-%dT%H:%M:%S"): #if all condition are meet + anti loopback if rule_result[1] == 0: #if not ddx rule print("rule " + rule + " is triggered") @@ -263,7 +264,7 @@ def rules_processor(sensor): Thread(target=sendRequest, args=["/api/" + bridge_config["rules"][rule]["owner"] + action["address"], action["method"], json.dumps(action["body"])]).start() else: #if ddx rule print("ddx rule " + rule + " will be re validated after " + str(rule_result[1]) + " seconds") - Thread(target=ddx_recheck, args=[rule, sensor, rule_result[1]]).start() + Thread(target=ddxRecheck, args=[rule, sensor, rule_result[1]]).start() def sendRequest(url, method, data, time_out=3, delay=0): if delay != 0: @@ -339,21 +340,21 @@ def convert_xy(x, y, bri): #needed for milight hub that don't work with xy value def sendLightRequest(light, data): payload = {} - if light in lights_address: - if lights_address[light]["protocol"] == "native": #ESP8266 light or strip - url = "http://" + lights_address[light]["ip"] + "/set?light=" + str(lights_address[light]["light_nr"]); + if light in bridge_config["lights_address"]: + if bridge_config["lights_address"][light]["protocol"] == "native": #ESP8266 light or strip + url = "http://" + bridge_config["lights_address"][light]["ip"] + "/set?light=" + str(bridge_config["lights_address"][light]["light_nr"]); method = 'GET' for key, value in data.iteritems(): if key == "xy": url += "&x=" + str(value[0]) + "&y=" + str(value[1]) else: url += "&" + key + "=" + str(value) - elif lights_address[light]["protocol"] == "hue": #Original Hue light - url = "http://" + lights_address[light]["ip"] + "/api/" + lights_address[light]["username"] + "/lights/" + lights_address[light]["light_id"] + "/state" + elif bridge_config["lights_address"][light]["protocol"] == "hue" or bridge_config["lights_address"][light]["protocol"] == "deconz": #Original Hue light or Deconz light + url = "http://" + bridge_config["lights_address"][light]["ip"] + "/api/" + bridge_config["lights_address"][light]["username"] + "/lights/" + bridge_config["lights_address"][light]["light_id"] + "/state" method = 'PUT' payload = data - elif lights_address[light]["protocol"] == "milight": #MiLight bulb - url = "http://" + lights_address[light]["ip"] + "/gateways/" + lights_address[light]["device_id"] + "/" + lights_address[light]["mode"] + "/" + str(lights_address[light]["group"]); + elif bridge_config["lights_address"][light]["protocol"] == "milight": #MiLight bulb + url = "http://" + bridge_config["lights_address"][light]["ip"] + "/gateways/" + bridge_config["lights_address"][light]["device_id"] + "/" + bridge_config["lights_address"][light]["mode"] + "/" + str(bridge_config["lights_address"][light]["group"]); method = 'PUT' for key, value in data.iteritems(): if key == "on": @@ -370,8 +371,8 @@ def sendLightRequest(light, data): payload["color"] = {} (payload["color"]["r"], payload["color"]["g"], payload["color"]["b"]) = convert_xy(value[0], value[1], bridge_config["lights"][light]["state"]["bri"]) print(json.dumps(payload)) - elif lights_address[light]["protocol"] == "ikea_tradfri": #IKEA Tradfri bulb - url = "coaps://" + lights_address[light]["ip"] + ":5684/15001/" + str(lights_address[light]["device_id"]) + elif bridge_config["lights_address"][light]["protocol"] == "ikea_tradfri": #IKEA Tradfri bulb + url = "coaps://" + bridge_config["lights_address"][light]["ip"] + ":5684/15001/" + str(bridge_config["lights_address"][light]["device_id"]) for key, value in data.iteritems(): if key == "on": payload["5850"] = int(value) @@ -397,13 +398,13 @@ def sendLightRequest(light, data): pprint(payload) try: - if lights_address[light]["protocol"] == "ikea_tradfri": + if bridge_config["lights_address"][light]["protocol"] == "ikea_tradfri": if "transitiontime" in payload: transitiontime = payload["transitiontime"] else: transitiontime = 4 for key, value in payload.iteritems(): #ikea bulbs don't accept all arguments at once - print(check_output("./coap-client-linux -m put -u \"Client_identity\" -k \"" + lights_address[light]["security_code"] + "\" -e '{ \"3311\": [" + json.dumps({key : value, "5712": transitiontime}) + "] }' \"" + url + "\"", shell=True).split("\n")[3]) + print(check_output("./coap-client-linux -m put -u \"Client_identity\" -k \"" + bridge_config["lights_address"][light]["security_code"] + "\" -e '{ \"3311\": [" + json.dumps({key : value, "5712": transitiontime}) + "] }' \"" + url + "\"", shell=True).split("\n")[3]) sleep(0.5) else: sendRequest(url, method, json.dumps(payload)) @@ -414,7 +415,7 @@ def sendLightRequest(light, data): bridge_config["lights"][light]["state"]["reachable"] = True print("LightRequest: " + url) -def update_group_stats(light): #set group stats based on lights status in that group +def updateGroupStats(light): #set group stats based on lights status in that group for group in bridge_config["groups"]: if light in bridge_config["groups"][group]["lights"]: for key, value in bridge_config["lights"][light]["state"].iteritems(): @@ -433,13 +434,13 @@ def update_group_stats(light): #set group stats based on lights status in that g 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")} -def scan_for_lights(): #scan for ESP8266 lights and strips +def scanForLights(): #scan for ESP8266 lights and strips print(json.dumps([{"success": {"/lights": "Searching for new devices"}}], sort_keys=True, indent=4, separators=(',', ': '))) #return all host that listen on port 80 - device_ips = check_output("nmap " + get_ip_address() + "/24 -p80 --open -n | grep report | cut -d ' ' -f5", shell=True).split("\n") + device_ips = check_output("nmap " + getIpAddress() + "/24 -p80 --open -n | grep report | cut -d ' ' -f5", shell=True).split("\n") del device_ips[-1] #delete last empty element in list for ip in device_ips: - if ip != get_ip_address(): + if ip != getIpAddress(): try: f = urllib2.urlopen("http://" + ip + "/detect") device_data = json.loads(f.read()) @@ -449,29 +450,28 @@ def scan_for_lights(): #scan for ESP8266 lights and strips for light in bridge_config["lights"].iterkeys(): if bridge_config["lights"][light]["uniqueid"].startswith( device_data["mac"] ): device_exist = True - lights_address[light]["ip"] = ip + bridge_config["lights_address"][light]["ip"] = ip if not device_exist: print("is a new device") for x in xrange(1, int(device_data["lights"]) + 1): - i = 1 - while (str(i)) in bridge_config["lights"]: - i += 1 + new_light_id = nextFreeId("lights") modelid = "LCT001" if device_data["type"] == "strip": modelid = "LST001" elif device_data["type"] == "generic": modelid = "LCT003" - bridge_config["lights"][str(i)] = {"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": "Hue " + device_data["type"] + " " + device_data["hue"] + " " + str(x), "uniqueid": device_data["mac"] + "-" + str(x), "modelid": modelid, "swversion": "66009461"} - new_lights.update({str(i): {"name": "Hue " + device_data["type"] + " " + device_data["hue"] + " " + str(x)}}) - lights_address[str(i)] = {"ip": ip, "light_nr": x, "protocol": "native"} + 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": "Hue " + device_data["type"] + " " + device_data["hue"] + " " + str(x), "uniqueid": device_data["mac"] + "-" + str(x), "modelid": modelid, "swversion": "66009461"} + new_lights.update({new_light_id: {"name": "Hue " + device_data["type"] + " " + device_data["hue"] + " " + str(x)}}) + bridge_config["lights_address"][new_light_id] = {"ip": ip, "light_nr": x, "protocol": "native"} except Exception as e: print(ip + " is unknow device " + str(e)) + scanDeconz() def syncWithLights(): #update Hue Bridge lights states - for light in lights_address: - if lights_address[light]["protocol"] == "native": + for light in bridge_config["lights_address"]: + if bridge_config["lights_address"][light]["protocol"] == "native": try: - light_data = json.loads(sendRequest("http://" + lights_address[light]["ip"] + "/get?light=" + str(lights_address[light]["light_nr"]), "GET", "{}", 0.5)) + light_data = json.loads(sendRequest("http://" + bridge_config["lights_address"][light]["ip"] + "/get?light=" + str(bridge_config["lights_address"][light]["light_nr"]), "GET", "{}", 0.5)) except: bridge_config["lights"][light]["state"]["reachable"] = False bridge_config["lights"][light]["state"]["on"] = False @@ -479,11 +479,11 @@ def syncWithLights(): #update Hue Bridge lights states else: bridge_config["lights"][light]["state"]["reachable"] = True bridge_config["lights"][light]["state"].update(light_data) - elif lights_address[light]["protocol"] == "hue": - light_data = json.loads(sendRequest("http://" + lights_address[light]["ip"] + "/api/" + lights_address[light]["username"] + "/lights/" + lights_address[light]["light_id"] + "/state"), "GET", "{}", 1) + elif bridge_config["lights_address"][light]["protocol"] == "hue": + light_data = json.loads(sendRequest("http://" + bridge_config["lights_address"][light]["ip"] + "/api/" + bridge_config["lights_address"][light]["username"] + "/lights/" + bridge_config["lights_address"][light]["light_id"] + "/state"), "GET", "{}", 1) bridge_config["lights"][light]["state"].update(light_data) - elif lights_address[light]["protocol"] == "ikea_tradfri": - light_stats = json.loads(check_output("./coap-client-linux -m get -u \"Client_identity\" -k \"" + lights_address[light]["security_code"] + "\" \"coaps://" + lights_address[light]["ip"] + ":5684/15001/" + str(lights_address[light]["device_id"]) +"\"", shell=True).split("\n")[3]) + elif bridge_config["lights_address"][light]["protocol"] == "ikea_tradfri": + light_stats = json.loads(check_output("./coap-client-linux -m get -u \"Client_identity\" -k \"" + bridge_config["lights_address"][light]["security_code"] + "\" \"coaps://" + bridge_config["lights_address"][light]["ip"] + ":5684/15001/" + str(bridge_config["lights_address"][light]["device_id"]) +"\"", shell=True).split("\n")[3]) bridge_config["lights"][light]["state"]["on"] = bool(light_stats["3311"][0]["5850"]) bridge_config["lights"][light]["state"]["bri"] = light_stats["3311"][0]["5851"] if "5706" in light_stats["3311"][0]: @@ -496,13 +496,105 @@ def syncWithLights(): #update Hue Bridge lights states else: bridge_config["lights"][light]["state"]["ct"] = 470 +def longPressButton(sensor, buttonevent): + print("long press detected") + sleep(1) + while bridge_config["sensors"][sensor]["state"]["buttonevent"] == buttonevent: + print("still pressed") + sensors_state[sensor]["state"]["lastupdated"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") + rulesProcessor(sensor) + sleep(0.9) + return + + +def websocketClient(): + from ws4py.client.threadedclient import WebSocketClient + class EchoClient(WebSocketClient): + def opened(self): + self.send("hello") + + def closed(self, code, reason=None): + print(("deconz websocket disconnected", code, reason)) + + def received_message(self, m): + print(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: + bridge_config["sensors"][bridge_sensor_id]["state"].update(message["state"]) + for key in message["state"].iterkeys(): + sensors_state[bridge_sensor_id]["state"][key] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + rulesProcessor(bridge_sensor_id) + if "buttonevent" in message["state"]: + if message["state"]["buttonevent"] in [2001, 3001, 4001, 5001]: + Thread(target=longPressButton, args=[bridge_sensor_id, message["state"]["buttonevent"]]).start() + elif "config" in message: + bridge_config["sensors"][bridge_sensor_id]["config"].update(message["config"]) + + except: + print("unable to process the request" + m) + + try: + ws = EchoClient('ws://127.0.0.1:' + str(bridge_config["deconz"]["websocketport"])) + ws.connect() + ws.run_forever() + except KeyboardInterrupt: + ws.close() + +def scanDeconz(): + if bridge_config["deconz"]["enabled"]: + if "port" in bridge_config["deconz"]: + port = bridge_config["deconz"]["port"] + else: + port = 8080 + + if "username" not in bridge_config["deconz"]: + try: + registration = json.loads(sendRequest("http://127.0.0.1:" + str(port) + "/api", "POST", "{\"username\": \"283145a4e198cc6535\", \"devicetype\":\"Hue Emulator\"}")) + except: + print("registration fail, is the link button pressed?") + return + if "success" in registration[0]: + bridge_config["deconz"]["username"] = registration[0]["success"]["username"] + deconz_config = json.loads(sendRequest("http://127.0.0.1:" + str(port) + "/api/" + bridge_config["deconz"]["username"] + "/config", "GET", "{}")) + bridge_config["deconz"]["websocketport"] = deconz_config["websocketport"] + registered_deconz_lights = [] + for light in bridge_config["lights_address"]: + if bridge_config["lights_address"][light]["protocol"] == "deconz": + registered_deconz_lights.append( bridge_config["lights_address"][light]["light_id"] ) + deconz_lights = json.loads(sendRequest("http://127.0.0.1:" + str(port) + "/api/" + bridge_config["deconz"]["username"] + "/lights", "GET", "{}")) + for light in deconz_lights: + if light not in registered_deconz_lights: + new_light_id = nextFreeId("lights") + print("register new light " + new_light_id) + bridge_config["lights"][new_light_id] = deconz_lights[light] + bridge_config["lights_address"][new_light_id] = {"username": bridge_config["deconz"]["username"], "light_id": light, "ip": "127.0.0.1:" + str(port), "protocol": "deconz"} + deconz_sensors = json.loads(sendRequest("http://127.0.0.1:" + str(port) + "/api/" + bridge_config["deconz"]["username"] + "/sensors", "GET", "{}")) + for sensor in deconz_sensors: + if sensor not in bridge_config["deconz"]["sensors"]: + new_sensor_id = nextFreeId("sensors") + if deconz_sensors[sensor]["modelid"] == "TRADFRI remote control": + print("register TRADFRI remote control") + bridge_config["sensors"][new_sensor_id] = {"config": deconz_sensors[sensor]["config"], "manufacturername": deconz_sensors[sensor]["manufacturername"], "modelid": deconz_sensors[sensor]["modelid"], "name": deconz_sensors[sensor]["name"], "state": deconz_sensors[sensor]["state"], "swversion": deconz_sensors[sensor]["swversion"], "type": deconz_sensors[sensor]["type"], "uniqueid": deconz_sensors[sensor]["uniqueid"]} + bridge_config["deconz"]["sensors"][sensor] = {"bridgeid": new_sensor_id} + elif deconz_sensors[sensor]["modelid"] == "TRADFRI motion sensor": + print("register TRADFRI remote control as Philips Motion Sensor") + newMotionSensorId = addHueMotionSensor() + bridge_config["deconz"]["sensors"][sensor] = {"bridgeid": newMotionSensorId} + + + + + def description(): return """ 1 0 -http://""" + get_ip_address() + """:80/ +http://""" + getIpAddress() + """:80/ urn:schemas-upnp-org:device:Basic:1 Philips hue @@ -534,7 +626,7 @@ def description(): """ -def webform_tradfri(): +def webformTradfri(): return """ @@ -555,6 +647,37 @@ def webform_tradfri(): """ +def webformIndex(): + content = """ + + + + +Deconz Setup + + + +
+
+Deconz Switches Setup\n""" + for deconzSensor in bridge_config["deconz"]["sensors"].iterkeys(): + if bridge_config["sensors"][bridge_config["deconz"]["sensors"][deconzSensor]["bridgeid"]]["modelid"] == "TRADFRI remote control": + content += "
\n" + content += "\n" + content += "\n" + content += "
\n" + content += """
+
+ +
+
+ +""" + return content + def webform_milight(): return """ @@ -620,17 +743,19 @@ def webform_hue(): """ -def update_all_lights(): +def updateAllLights(): ## apply last state on startup to all bulbs, usefull if there was a power outage - for light in lights_address: + for light in bridge_config["lights_address"]: payload = {} payload["on"] = bridge_config["lights"][light]["state"]["on"] payload["bri"] = bridge_config["lights"][light]["state"]["bri"] - if bridge_config["lights"][light]["state"]["colormode"] in ["xy", "ct"]: - payload[bridge_config["lights"][light]["state"]["colormode"]] = bridge_config["lights"][light]["state"][bridge_config["lights"][light]["state"]["colormode"]] - elif bridge_config["lights"][light]["state"]["colormode"] == "hs": - payload["hue"] = bridge_config["lights"][light]["state"]["hue"] - payload["sat"] = bridge_config["lights"][light]["state"]["sat"] + if "colormode" in bridge_config["lights"][light]["state"]: + if bridge_config["lights"][light]["state"]["colormode"] in ["xy", "ct"]: + payload[bridge_config["lights"][light]["state"]["colormode"]] = bridge_config["lights"][light]["state"][bridge_config["lights"][light]["state"]["colormode"]] + elif bridge_config["lights"][light]["state"]["colormode"] == "" and "hue" in bridge_config["lights"][light]["state"]: + payload["hue"] = bridge_config["lights"][light]["state"]["hue"] + payload["sat"] = bridge_config["lights"][light]["state"]["sat"] + Thread(target=sendLightRequest, args=[light, payload]).start() sleep(0.5) print("update status for light " + light) @@ -659,28 +784,24 @@ def do_GET(self): lights_found += 1 #register new tradfri light print("register tradfi light " + device_parameters["9001"]) - i = 1 - while (str(i)) in bridge_config["lights"]: - i += 1 - bridge_config["lights"][str(i)] = {"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"} - new_lights.update({str(i): {"name": device_parameters["9001"]}}) - lights_address[str(i)] = {"device_id": device, "security_code": get_parameters["code"][0], "ip": get_parameters["ip"][0], "protocol": "ikea_tradfri"} + new_light_id = nextFreeId("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"} + new_lights.update({new_light_id: {"name": device_parameters["9001"]}}) + bridge_config["lights_address"][new_light_id] = {"device_id": device, "security_code": get_parameters["code"][0], "ip": get_parameters["ip"][0], "protocol": "ikea_tradfri"} if lights_found == 0: - self.wfile.write(webform_tradfri() + "
No lights where found") + self.wfile.write(webformTradfri() + "
No lights where found") else: - self.wfile.write(webform_tradfri() + "
" + str(lights_found) + " lights where found") + self.wfile.write(webformTradfri() + "
" + str(lights_found) + " lights where found") else: - self.wfile.write(webform_tradfri()) + self.wfile.write(webformTradfri()) elif self.path.startswith("/milight"): #setup milight bulb get_parameters = parse_qs(urlparse(self.path).query) if "device_id" in get_parameters: #register new mi-light - i = 1 - while (str(i)) in bridge_config["lights"]: - i += 1 - bridge_config["lights"][str(i)] = {"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": "MiLight " + get_parameters["mode"][0] + " " + get_parameters["device_id"][0], "uniqueid": "1a2b3c4" + str(random.randrange(0, 99)), "modelid": "LCT001", "swversion": "66009461"} - new_lights.update({str(i): {"name": "MiLight " + get_parameters["mode"][0] + " " + get_parameters["device_id"][0]}}) - lights_address[str(i)] = {"device_id": get_parameters["device_id"][0], "mode": get_parameters["mode"][0], "group": int(get_parameters["group"][0]), "ip": get_parameters["ip"][0], "protocol": "milight"} + new_light_id = nextFreeId("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": "MiLight " + get_parameters["mode"][0] + " " + get_parameters["device_id"][0], "uniqueid": "1a2b3c4" + str(random.randrange(0, 99)), "modelid": "LCT001", "swversion": "66009461"} + new_lights.update({new_light_id: {"name": "MiLight " + get_parameters["mode"][0] + " " + get_parameters["device_id"][0]}}) + bridge_config["lights_address"][new_light_id] = {"device_id": get_parameters["device_id"][0], "mode": get_parameters["mode"][0], "group": int(get_parameters["group"][0]), "ip": get_parameters["ip"][0], "protocol": "milight"} self.wfile.write(webform_milight() + "
Light added") else: self.wfile.write(webform_milight()) @@ -692,11 +813,9 @@ def do_GET(self): hue_lights = json.loads(sendRequest("http://" + get_parameters["ip"][0] + "/api/" + response[0]["success"]["username"] + "/lights", "GET", "{}")) lights_found = 0 for hue_light in hue_lights: - i = 1 - while (str(i)) in bridge_config["lights"]: - i += 1 - bridge_config["lights"][str(i)] = hue_lights[hue_light] - lights_address[str(i)] = {"username": response[0]["success"]["username"], "light_id": hue_light, "ip": get_parameters["ip"][0], "protocol": "hue"} + new_light_id = nextFreeId("lights") + bridge_config["lights"][new_light_id] = hue_lights[hue_light] + bridge_config["lights_address"][new_light_id] = {"username": response[0]["success"]["username"], "light_id": hue_light, "ip": get_parameters["ip"][0], "protocol": "hue"} lights_found += 1 if lights_found == 0: self.wfile.write(webform_hue() + "
No lights where found") @@ -706,6 +825,29 @@ def do_GET(self): self.wfile.write(webform_hue() + "
unable to connect to hue bridge") else: self.wfile.write(webform_hue()) + elif self.path.startswith("/deconz"): #setup imported deconz sensors + get_parameters = parse_qs(urlparse(self.path).query) + pprint(get_parameters) + #clean all rules related to deconz Switches + sensorsResourcelinks = [] + if get_parameters: + for resourcelink in bridge_config["resourcelinks"].iterkeys(): + if bridge_config["resourcelinks"][resourcelink]["classid"] == 15555: + sensorsResourcelinks.append(resourcelink) + for link in bridge_config["resourcelinks"][resourcelink]["links"]: + pices = link.split('/') + if pices[1] == "rules": + try: + del bridge_config["rules"][pices[2]] + except: + print("unable to delete the rule " + pices[2]) + for resourcelink in sensorsResourcelinks: + del bridge_config["resourcelinks"][resourcelink] + for key in get_parameters.iterkeys(): + addTradfriRemote(key, get_parameters[key][0]) + else: + scanDeconz() + self.wfile.write(webformIndex()) elif self.path.startswith("/switch"): #request from an ESP8266 switch or sensor get_parameters = parse_qs(urlparse(self.path).query) pprint(get_parameters) @@ -716,18 +858,14 @@ def do_GET(self): sensor_is_new = False if sensor_is_new: print("registering new sensor " + get_parameters["devicetype"][0]) - i = 1 #find first empty sensor id - while (str(i)) in bridge_config["sensors"]: - i += 1 + new_sensor_id = nextFreeId("sensors") if get_parameters["devicetype"][0] == "ZLLSwitch" or get_parameters["devicetype"][0] == "ZGPSwitch": print("ZLLSwitch") - bridge_config["sensors"][str(i)] = {"state": {"buttonevent": 0, "lastupdated": "none"}, "config": {"on": True, "battery": 100, "reachable": True}, "name": "Dimmer Switch" if get_parameters["devicetype"][0] == "ZLLSwitch" else "Tap Switch", "type": get_parameters["devicetype"][0], "modelid": "RWL021" if get_parameters["devicetype"][0] == "ZLLSwitch" else "ZGPSWITCH", "manufacturername": "Philips", "swversion": "5.45.1.17846" if get_parameters["devicetype"][0] == "ZLLSwitch" else "", "uniqueid": get_parameters["mac"][0]} + bridge_config["sensors"][new_sensor_id] = {"state": {"buttonevent": 0, "lastupdated": "none"}, "config": {"on": True, "battery": 100, "reachable": True}, "name": "Dimmer Switch" if get_parameters["devicetype"][0] == "ZLLSwitch" else "Tap Switch", "type": get_parameters["devicetype"][0], "modelid": "RWL021" if get_parameters["devicetype"][0] == "ZLLSwitch" else "ZGPSWITCH", "manufacturername": "Philips", "swversion": "5.45.1.17846" if get_parameters["devicetype"][0] == "ZLLSwitch" else "", "uniqueid": get_parameters["mac"][0]} elif get_parameters["devicetype"][0] == "ZLLPresence": print("ZLLPresence") - bridge_config["sensors"][str(i)] = {"name": "Hue temperature sensor 1", "uniqueid": get_parameters["mac"][0] + ":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"} - bridge_config["sensors"][str(i + 1)] = {"name": "Entrance Lights sensor", "uniqueid": get_parameters["mac"][0] + ":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"][str(i + 2)] = {"name": "Hue ambient light sensor 1", "uniqueid": get_parameters["mac"][0] + ":d0:5b-02-0400", "type": "ZLLLightLevel", "swversion": "6.1.0.18912", "state": {"dark": None, "daylight": None, "lightlevel": None, "lastupdated": "none"}, "manufacturername": "Philips", "config": {"on": False,"battery": 100, "reachable": True, "alert": "none", "tholddark": 21597, "tholdoffset": 7000, "ledindication": False, "usertest": False, "pending": []}, "modelid": "SML001"} - generate_sensors_state() + addHueMotionSensor() + generateSensorsState() else: #switch action request for sensor in bridge_config["sensors"]: if bridge_config["sensors"][sensor]["uniqueid"].startswith(get_parameters["mac"][0]) and bridge_config["sensors"][sensor]["config"]["on"]: #match senser id based on mac address @@ -735,35 +873,32 @@ def do_GET(self): if bridge_config["sensors"][sensor]["type"] == "ZLLSwitch" or bridge_config["sensors"][sensor]["type"] == "ZGPSwitch": bridge_config["sensors"][sensor]["state"].update({"buttonevent": get_parameters["button"][0], "lastupdated": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")}) sensors_state[sensor]["state"]["lastupdated"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - rules_processor(sensor) + rulesProcessor(sensor) elif bridge_config["sensors"][sensor]["type"] == "ZLLPresence" and "presence" in get_parameters: if str(bridge_config["sensors"][sensor]["state"]["presence"]).lower() != get_parameters["presence"][0]: sensors_state[sensor]["state"]["presence"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") bridge_config["sensors"][sensor]["state"].update({"presence": True if get_parameters["presence"][0] == "true" else False, "lastupdated": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")}) - rules_processor(sensor) + rulesProcessor(sensor) #if alarm is activ trigger the alarm - if "virtual_light" in alarm_config and bridge_config["lights"][alarm_config["virtual_light"]]["state"]["on"] and bridge_config["sensors"][sensor]["state"]["presence"] == True: - send_email(bridge_config["sensors"][sensor]["name"]) + if "virtual_light" in bridge_config["alarm_config"] and bridge_config["lights"][bridge_config["alarm_config"]["virtual_light"]]["state"]["on"] and bridge_config["sensors"][sensor]["state"]["presence"] == True: + sendEmail(bridge_config["sensors"][sensor]["name"]) #triger_horn() need development elif bridge_config["sensors"][sensor]["type"] == "ZLLLightLevel" and "lightlevel" in get_parameters: if str(bridge_config["sensors"][sensor]["state"]["dark"]).lower() != get_parameters["dark"][0]: sensors_state[sensor]["state"]["dark"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") bridge_config["sensors"][sensor]["state"].update({"lightlevel":int(get_parameters["lightlevel"][0]), "dark":True if get_parameters["dark"][0] == "true" else False, "daylight":True if get_parameters["daylight"][0] == "true" else False, "lastupdated": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S")}) - rules_processor(sensor) #process the rules to perform the action configured by application + rulesProcessor(sensor) #process the rules to perform the action configured by application else: url_pices = self.path.split('/') if url_pices[2] in bridge_config["config"]["whitelist"]: #if username is in whitelist bridge_config["config"]["UTC"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") bridge_config["config"]["localtime"] = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") if len(url_pices) == 3: #print entire config - self.wfile.write(json.dumps(bridge_config)) + self.wfile.write(json.dumps({"lights": bridge_config["lights"], "groups": bridge_config["groups"], "config": bridge_config["config"], "scenes": bridge_config["scenes"], "schedules": bridge_config["schedules"], "rules": bridge_config["rules"], "sensors": bridge_config["sensors"], "resourcelinks": bridge_config["resourcelinks"]})) elif len(url_pices) == 4: #print specified object config - if url_pices[3] == "capabilities": - self.wfile.write(json.dumps(capabilities)) - else: - if url_pices[3] == "lights": #add changes from IKEA Tradfri gateway to bridge - syncWithLights() - self.wfile.write(json.dumps(bridge_config[url_pices[3]])) + if url_pices[3] == "lights": #add changes from IKEA Tradfri gateway to bridge + syncWithLights() + self.wfile.write(json.dumps(bridge_config[url_pices[3]])) elif len(url_pices) == 5: if url_pices[4] == "new": #return new lights and sensors only new_lights.update({"lastscan": datetime.now().strftime("%Y-%m-%dT%H:%M:%S")}) @@ -791,14 +926,12 @@ def do_POST(self): if url_pices[2] in bridge_config["config"]["whitelist"]: if ((url_pices[3] == "lights" or url_pices[3] == "sensors") and not bool(post_dictionary)): #if was a request to scan for lights of sensors - Thread(target=scan_for_lights).start() + Thread(target=scanForLights).start() sleep(7) #give no more than 7 seconds for light scanning (otherwise will face app disconnection timeout) self.wfile.write(json.dumps([{"success": {"/" + url_pices[3]: "Searching for new devices"}}])) else: #create object # find the first unused id for new object - i = 1 - while (str(i)) in bridge_config[url_pices[3]]: - i += 1 + new_object_id = nextFreeId(url_pices[3]) if url_pices[3] == "scenes": post_dictionary.update({"lightstates": {}, "version": 2, "picture": "", "lastupdated": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S"), "owner" :url_pices[2]}) if "locked" not in post_dictionary: @@ -820,14 +953,14 @@ def do_POST(self): post_dictionary.update({"state": {"status": 0}}) elif url_pices[3] == "resourcelinks": post_dictionary.update({"owner" :url_pices[2]}) - generate_sensors_state() - bridge_config[url_pices[3]][str(i)] = post_dictionary - print(json.dumps([{"success": {"id": str(i)}}], sort_keys=True, indent=4, separators=(',', ': '))) - self.wfile.write(json.dumps([{"success": {"id": str(i)}}], sort_keys=True, indent=4, separators=(',', ': '))) + generateSensorsState() + bridge_config[url_pices[3]][new_object_id] = post_dictionary + print(json.dumps([{"success": {"id": new_object_id}}], sort_keys=True, indent=4, separators=(',', ': '))) + self.wfile.write(json.dumps([{"success": {"id": new_object_id}}], sort_keys=True, indent=4, separators=(',', ': '))) else: self.wfile.write(json.dumps([{"error": {"type": 1, "address": self.path, "description": "unauthorized user" }}],sort_keys=True, indent=4, separators=(',', ': '))) print(json.dumps([{"error": {"type": 1, "address": self.path, "description": "unauthorized user" }}],sort_keys=True, indent=4, separators=(',', ': '))) - elif "devicetype" in post_dictionary: #this must be a new device registration + elif "devicetype" in post_dictionary and bridge_config["config"]["linkbutton"]: #this must be a new device registration #create new user hash s = hashlib.new('ripemd160', post_dictionary["devicetype"][0] ).digest() username = s.encode('hex') @@ -835,12 +968,13 @@ def do_POST(self): self.wfile.write(json.dumps([{"success": {"username": username}}], sort_keys=True, indent=4, separators=(',', ': '))) print(json.dumps([{"success": {"username": username}}], sort_keys=True, indent=4, separators=(',', ': '))) self.end_headers() - save_config() + saveConfig() def do_PUT(self): self._set_headers() print ("in PUT method") self.data_string = self.rfile.read(int(self.headers['Content-Length'])) + print(self.data_string) put_dictionary = json.loads(self.data_string) url_pices = self.path.split('/') if url_pices[2] in bridge_config["config"]["whitelist"]: @@ -865,7 +999,7 @@ def do_PUT(self): del bridge_config["scenes"][url_pices[4]]["lightstates"][light]["sat"] if bridge_config["lights"][light]["state"]["colormode"] in ["ct", "xy"]: 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"]] - elif bridge_config["lights"][light]["state"]["colormode"] == "hs": + elif bridge_config["lights"][light]["state"]["colormode"] == "hs" and "hue" in bridge_config["scenes"][url_pices[4]]["lightstates"][light]: bridge_config["scenes"][url_pices[4]]["lightstates"][light]["hue"] = bridge_config["lights"][light]["state"]["hue"] bridge_config["scenes"][url_pices[4]]["lightstates"][light]["sat"] = bridge_config["lights"][light]["state"]["sat"] @@ -876,7 +1010,7 @@ def do_PUT(self): bridge_config[url_pices[3]][url_pices[4]][key].update(value) else: bridge_config[url_pices[3]][url_pices[4]][key] = value - rules_processor(url_pices[4]) + rulesProcessor(url_pices[4]) else: bridge_config[url_pices[3]][url_pices[4]].update(put_dictionary) response_location = "/" + url_pices[3] + "/" + url_pices[4] + "/" @@ -892,7 +1026,7 @@ def do_PUT(self): elif "hue" or "sat" in bridge_config["scenes"][put_dictionary["scene"]]["lightstates"][light]: bridge_config["lights"][light]["state"]["colormode"] = "hs" Thread(target=sendLightRequest, args=[light, bridge_config["scenes"][put_dictionary["scene"]]["lightstates"][light]]).start() - update_group_stats(light) + updateGroupStats(light) elif "bri_inc" in put_dictionary: bridge_config["groups"][url_pices[4]]["action"]["bri"] += int(put_dictionary["bri_inc"]) if bridge_config["groups"][url_pices[4]]["action"]["bri"] > 254: @@ -905,6 +1039,18 @@ def do_PUT(self): for light in bridge_config["groups"][url_pices[4]]["lights"]: bridge_config["lights"][light]["state"].update(put_dictionary) Thread(target=sendLightRequest, args=[light, put_dictionary]).start() + elif "ct_inc" in put_dictionary: + bridge_config["groups"][url_pices[4]]["action"]["ct"] += int(put_dictionary["ct_inc"]) + if bridge_config["groups"][url_pices[4]]["action"]["ct"] > 500: + bridge_config["groups"][url_pices[4]]["action"]["ct"] = 500 + elif bridge_config["groups"][url_pices[4]]["action"]["ct"] < 153: + bridge_config["groups"][url_pices[4]]["action"]["ct"] = 153 + bridge_config["groups"][url_pices[4]]["state"]["ct"] = bridge_config["groups"][url_pices[4]]["action"]["ct"] + del put_dictionary["ct_inc"] + put_dictionary.update({"ct": bridge_config["groups"][url_pices[4]]["action"]["ct"]}) + for light in bridge_config["groups"][url_pices[4]]["lights"]: + bridge_config["lights"][light]["state"].update(put_dictionary) + Thread(target=sendLightRequest, args=[light, put_dictionary]).start() elif url_pices[4] == "0": for light in bridge_config["lights"].iterkeys(): bridge_config["lights"][light]["state"].update(put_dictionary) @@ -928,7 +1074,7 @@ def do_PUT(self): bridge_config["lights"][url_pices[4]]["state"]["colormode"] = key elif key in ["hue", "sat"]: bridge_config["lights"][url_pices[4]]["state"]["colormode"] = "hs" - update_group_stats(url_pices[4]) + updateGroupStats(url_pices[4]) if not url_pices[4] == "0": #group 0 is virtual, must not be saved in bridge configuration try: bridge_config[url_pices[3]][url_pices[4]][url_pices[5]].update(put_dictionary) @@ -937,7 +1083,7 @@ def do_PUT(self): if url_pices[3] == "sensors" and url_pices[5] == "state": for key in put_dictionary.iterkeys(): sensors_state[url_pices[4]]["state"].update({key: datetime.now().strftime("%Y-%m-%dT%H:%M:%S")}) - rules_processor(url_pices[4]) + rulesProcessor(url_pices[4]) response_location = "/" + url_pices[3] + "/" + url_pices[4] + "/" + url_pices[5] + "/" if len(url_pices) == 7: try: @@ -969,7 +1115,7 @@ def do_DELETE(self): print(link + " not found, very likely it was already deleted by app") del bridge_config[url_pices[3]][url_pices[4]] if url_pices[3] == "lights": - del lights_address[url_pices[4]] + del bridge_config["lights_address"][url_pices[4]] self.wfile.write(json.dumps([{"success": "/" + url_pices[3] + "/" + url_pices[4] + " deleted."}])) def run(server_class=HTTPServer, handler_class=S): @@ -979,15 +1125,18 @@ def run(server_class=HTTPServer, handler_class=S): httpd.serve_forever() if __name__ == "__main__": + if bridge_config["deconz"]["enabled"]: + scanDeconz() + Thread(target=websocketClient).start() try: - update_all_lights() - Thread(target=ssdp_search).start() - Thread(target=ssdp_broadcast).start() - Thread(target=scheduler_processor).start() + updateAllLights() + Thread(target=ssdpSearch).start() + Thread(target=ssdpBroadcast).start() + Thread(target=schedulerProcessor).start() run() except: print("server stopped") finally: run_service = False - save_config() + saveConfig() print ('config saved') diff --git a/BridgeEmulator/alarm_config.json b/BridgeEmulator/alarm_config.json deleted file mode 100644 index f6c02123..00000000 --- a/BridgeEmulator/alarm_config.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "mail_from": "your_email@gmail.com", - "mail_password": "pass", - "mail_recipients": [ - "first_recipient@mail.com" - ], - "mail_subject": "HUE ALARM TRIGGERED!", - "mail_username": "username@gmail.com", - "smtp_port": 465, - "smtp_server": "smtp.gmail.com" -} diff --git a/BridgeEmulator/config.json b/BridgeEmulator/config.json index 01143b7f..5fa460b7 100644 --- a/BridgeEmulator/config.json +++ b/BridgeEmulator/config.json @@ -1,4 +1,57 @@ { + "alarm_config": { + "mail_from": "your_email@gmail.com", + "mail_username": "", + "mail_password": "", + "mail_recipients": [ + "first_recipient@mail.com", + "second_recipient@mail.com" + ], + "mail_subject": "HUE ALARM TRIGGERED!", + "smtp_port": 465, + "smtp_server": "smtp.gmail.com" + }, + "capabilities": { + "groups": { + "available": 64 + }, + "lights": { + "available": 63 + }, + "resourcelinks": { + "available": 64 + }, + "rules": { + "actions": { + "available": 400 + }, + "available": 200, + "conditions": { + "available": 400 + } + }, + "scenes": { + "available": 200, + "lightstates": { + "available": 2048 + } + }, + "schedules": { + "available": 100 + }, + "sensors": { + "available": 63, + "clip": { + "available": 63 + }, + "zgp": { + "available": 63 + }, + "zll": { + "available": 63 + } + } + }, "config": { "UTC": "2017-04-26T20:16:20", "apiversion": "1.19.0", @@ -22,11 +75,16 @@ "whitelist": {}, "zigbeechannel": 15 }, + "deconz": { + "enabled": false, + "sensors": {} + }, "groups": {}, "lights": {}, "resourcelinks": {}, "rules": {}, "scenes": {}, "schedules": {}, - "sensors": {} + "sensors": {}, + "lights_address": {} } diff --git a/README.md b/README.md index c0aa87c2..7bea3a0f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ ## diyHue -This project emulates a Philips Hue Bridge that is able to control Hue lights (using original Hue Bridge), IKEA Tradfri lights (usign Tradfri Bridge), Mi-Light bulbs (using MiLight Hub), Neopixel strips (WS2812B and SK6812) and any cheep ESP8266 based bulb from market by replacing firmware with custom one. Is written in python and will run on all small boxes like RaspberryPi. There are provided sketches for Hue Dimmer Switch, Hue Tap Switch and Hue Motion Sensor. Lights are two-way synchronized so any change made from original Philips/Tradfri sensors and switches will be applied also to bridge emulator. +This project emulates a Philips Hue Bridge that is able to control ZigBee lights (using Raspbee module or original Hue Bridge or IKEA Tradfri Gateway), Mi-Light bulbs (using MiLight Hub), Neopixel strips (WS2812B and SK6812) and any cheep ESP8266 based bulb from market by replacing firmware with custom one. Is written in python and will run on all small boxes like RaspberryPi. There are provided sketches for Hue Dimmer Switch, Hue Tap Switch and Hue Motion Sensor. Lights are two-way synchronized so any change made from original Philips/Tradfri sensors and switches will be applied also to bridge emulator. ![diyHue ecosystem](https://raw.githubusercontent.com/mariusmotea/diyHue/develop/Images/hue-map.png) ### Requirements: - python - - nmap package for lights autodiscover ```sudo apt install namp``` + - nmap package for esp8266 lights autodiscover ```sudo apt install namp``` + - python ws4py package only if zigbee module is used ```sudo pip install ws4py``` + ## TO DO - Working directly with ZigBee lights, switches and sensors with RaspBee module @@ -27,6 +29,19 @@ This project emulates a Philips Hue Bridge that is able to control Hue lights (u ## Not working: - Home & Away futures (require remote api that is not public) +## ZIGBEE LIGHTS, SENSORS AND SWITCHES + Starting with version 2 the zigbee module is supported in order control zigbee lights directly and to be able to use zigbee switches and sensors (currently only IKEA Tradfri are supported). + Deconz installation (Warning GUI env required!): + 1. execute raspi-config and turn off the serial login as this will enter in conflict with deconz (do not disable also the hardware serial port) + 2. Follow the steps from here: https://github.com/dresden-elektronik/deconz-rest-plugin if you receive the error "/usr/include/c++/6/cstdlib:75:25: fatal error: stdlib.h: No such file or directory" then replace ```qmake``` command with ```qmake QMAKE_CFLAGS_ISYSTEM=``` + 3. edit deconz systemd script to bind on port 8080: ```sudo vim /etc/systemd/system/deconz.service``` replace ```--http-port=80``` with ```--http-port=8080``` + 4. start deconz service browse http://{hue emulator ip}:8080 and add all zigbee devices. This is done by clicking "Open network" in settings and then reset the devices. Don't configure any device in deconz. + 5. click "Unlock Gateway" in settings to allow hue emulator to register + 6. edit config.json and change the deconz => enabled to true + 7. start hue emulator (must in output the import of all zigbee devices) + + To configure IKEA Remotes open http://{hue emulator ip}/deconz . When you click save will look like noting happened, but all rules must be already created. Test the remotes. + To configure IKEA Motion Sensor open official Hue application and go to "Accesory Setup" ## HUE LIGHTS Open http://{bridgeIP}/hue, type the bridge ip and before to click "Save" press the link button on the Hue Bridge. The total number of lights copied to Bridge Emulator will be displayed. @@ -82,7 +97,8 @@ On sensor power on there will be a GET request sent to bridge , ex: http://{brid Exactly like switches the sensor will be registered on power on with GET request http://{bridgeIP}/switch?mac=xx:xx:xx:xx:xx:xx&devicetype=ZLLPresence and configuration will be done from Hue application. ESP8266 will wake up from deep sleep on every PIR positive signal on GPIO5 pin and at every 20 minutes to send light sensor data. Request example: http://{bridgeIP}/switch?mac=xx:xx:xx:xx:xx:xx&lightlevel=46900&dark=false&daylight=true&presence=true. Is important to choose a low power PIR that can run on batteries for many months. The PIR used in my example is HC-SR501, most common used in DIY projects. To increase the battery life i remove the voltage regulator to 3.3V because this become useless on batteries. GPIO4 will output +3V only when light level is measured to lower power consumption. ## ALARM -Is possible to receive email notification when one motion sensor is triggered while alarm is active. To configure the alarm you must first edit the file alarm_config.json and add your smtp credentials. On first execution HueEmulator.py will send a test email and if this is successful sent a new virtual light named "Alarm" will be automatically created. You will need to create a dummy room to control this virtual light. Turn this light on/off to enable/disable the alarm. + +Is possible to receive email notification when one motion sensor is triggered while alarm is active. To configure the alarm you must first edit the file config.json and add your smtp credentials. On first execution HueEmulator.py will send a test email and if this is successful sent a new virtual light named "Alarm" will be automatically created. You will need to create a dummy room to control this virtual light. Turn this light on/off to enable/disable the alarm. [![Youtube Demo](https://img.youtube.com/vi/c6MsG3oIehY/0.jpg)](https://www.youtube.com/watch?v=c6MsG3oIehY)