From 1439ddb67515f84206f8abf12806a73000fc98ec Mon Sep 17 00:00:00 2001 From: Silve Date: Fri, 17 Jan 2020 00:35:15 +0300 Subject: [PATCH 01/13] Implementation of Yeelight Music Mode Yeelight now supports Philips Entertainment API * Changed entertainment service method to pass Host IP argument; * Changed sendLightRequest method to pass entertainment Host IP to turn on music mode on yeelight devices before sending them any commands; + Implemented YeelightConnection class which is now used to communicate with Yeelight devices and to maintain Yeelight Music Mode connections; + Implemented dictionary logic to support dedicated yeelight connections; --- BridgeEmulator/HueEmulator3.py | 2 +- BridgeEmulator/functions/entertainment.py | 14 +- BridgeEmulator/functions/lightRequest.py | 4 +- BridgeEmulator/protocols/yeelight.py | 264 ++++++++++++++++------ 4 files changed, 207 insertions(+), 77 deletions(-) diff --git a/BridgeEmulator/HueEmulator3.py b/BridgeEmulator/HueEmulator3.py index c5558d712..1760676a8 100755 --- a/BridgeEmulator/HueEmulator3.py +++ b/BridgeEmulator/HueEmulator3.py @@ -1940,7 +1940,7 @@ def run(https, server_class=ThreadingSimpleServer, handler_class=S): Thread(target=ssdpBroadcast, args=[HOST_IP, HOST_HTTP_PORT, mac]).start() Thread(target=schedulerProcessor).start() Thread(target=syncWithLights, args=[bridge_config["lights"], bridge_config["lights_address"], bridge_config["config"]["whitelist"], bridge_config["groups"], off_if_unreachable]).start() - Thread(target=entertainmentService, args=[bridge_config["lights"], bridge_config["lights_address"], bridge_config["groups"]]).start() + Thread(target=entertainmentService, args=[bridge_config["lights"], bridge_config["lights_address"], bridge_config["groups"], HOST_IP]).start() Thread(target=run, args=[False]).start() if not args.no_serve_https: Thread(target=run, args=[True]).start() diff --git a/BridgeEmulator/functions/entertainment.py b/BridgeEmulator/functions/entertainment.py index 237f1bd75..ce04a7242 100644 --- a/BridgeEmulator/functions/entertainment.py +++ b/BridgeEmulator/functions/entertainment.py @@ -3,7 +3,7 @@ from functions.colors import convert_rgb_xy, convert_xy from functions.lightRequest import sendLightRequest -def entertainmentService(lights, addresses, groups): +def entertainmentService(lights, addresses, groups, host_ip): serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) serverSocket.settimeout(3) #Set a packet timeout that we catch later serverSocket.bind(('127.0.0.1', 2101)) @@ -45,20 +45,20 @@ def entertainmentService(lights, addresses, groups): if fremeID == 24: # => every seconds, increase in case the destination device is overloaded if r == 0 and g == 0 and b == 0: if lightStatus[lightId]["on"]: - sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, host_ip) lightStatus[lightId]["on"] = False elif lightStatus[lightId]["on"] == False: - sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, host_ip) lightStatus[lightId]["on"] = True elif abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) > 50: # to optimize, send brightness only of difference is bigger than this value - sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 3}, lights, addresses, host_ip) lightStatus[lightId]["bri"] = int((r + b + g) / 3) else: - sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, host_ip) fremeID += 1 if fremeID == 25: fremeID = 0 - i = i + 9 + i = i + 9 #TODO CHECK BOTTOM SEGMENT, THERE IS NO "i" INCREMENTATION elif data[14] == 1: #cie colorspace i = 16 while i < len(data): @@ -87,7 +87,7 @@ def entertainmentService(lights, addresses, groups): else: fremeID += 1 if fremeID == 24 : #24 = every seconds, increase in case the destination device is overloaded - sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses) + sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, host_ip) fremeID = 0 if len(nativeLights) is not 0: for ip in nativeLights.keys(): diff --git a/BridgeEmulator/functions/lightRequest.py b/BridgeEmulator/functions/lightRequest.py index 64ac43843..701f94a53 100755 --- a/BridgeEmulator/functions/lightRequest.py +++ b/BridgeEmulator/functions/lightRequest.py @@ -7,13 +7,15 @@ from time import sleep from functions.updateGroup import updateGroupStats -def sendLightRequest(light, data, lights, addresses): +def sendLightRequest(light, data, lights, addresses, entertainmentHostIP = None): payload = {} if light in addresses: protocol_name = addresses[light]["protocol"] for protocol in protocols: if "protocols." + protocol_name == protocol.__name__: try: + if entertainmentHostIP and protocol_name == "yeelight": + protocol.enableMusic(addresses[light]["ip"], entertainmentHostIP) light_state = protocol.set_light(addresses[light], lights[light], data) except Exception as e: lights[light]["state"]["reachable"] = False diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index c4bec757d..8e4a5e870 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -7,6 +7,8 @@ from functions import light_types, nextFreeId from functions.colors import convert_rgb_xy, convert_xy +Connections = {} + def discover(bridge_config, new_lights): group = ("239.255.255.250", 1982) message = "\r\n".join([ @@ -66,17 +68,17 @@ def discover(bridge_config, new_lights): sock.close() break -def command(ip, api_method, param): +def command(ip, light, api_method, param): + if ip in Connections: + c = Connections[ip] + else: + c = YeelightConnection(ip, light) + Connections[ip] = c try: - tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - tcp_socket.settimeout(5) - tcp_socket.connect((ip, int(55443))) - msg = json.dumps({"id": 1, "method": api_method, "params": param}) + "\r\n" - tcp_socket.send(msg.encode()) - tcp_socket.close() - except Exception as e: - logging.warning("Yeelight command error: %s", e) - + c.command(api_method, param) + finally: + if not c._music and c._connected: + c.disconnect() def set_light(address, light, data): method = 'TCP' @@ -111,67 +113,193 @@ def set_light(address, light, data): # see page 9 http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf # check if hue wants to change brightness for key, value in payload.items(): - command(address["ip"], key, value) + command(address["ip"], light, key, value) def get_light_state(address, light): - #logging.info("name is: " + light["name"]) - #if light["name"].find("desklamp") > 0: logging.info("is desk lamp") - state = {} - tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - tcp_socket.settimeout(5) - tcp_socket.connect((address["ip"], int(55443))) - msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" - tcp_socket.send(msg.encode()) - data = tcp_socket.recv(16 * 1024) - light_data = json.loads(data[:-2].decode("utf8"))["result"] - if light_data[0] == "on": #powerstate - state['on'] = True + ip = address["ip"] + if ip in Connections: + c = Connections[ip] else: - state['on'] = False - state["bri"] = int(int(light_data[1]) * 2.54) - #if ip[:-3] == "201" or ip[:-3] == "202": - if light["name"].find("desklamp") > 0: - msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - tcp_socket.send(msg_ct.encode()) - data = tcp_socket.recv(16 * 1024) - tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - if tempval > 369: tempval = 369 - state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - state["colormode"] = "ct" + c = YeelightConnection(ip, light) + Connections[ip] = c + try: + state = Connections[ip].get_state() + if not c._music and c._connected: + c.disconnect() + return state + finally: + if not c._music and c._connected: + c.disconnect() + +def enableMusic(ip, host_ip): + if ip in Connections: + c = Connections[ip] + if not c._music: + c.enableMusic(host_ip) else: - msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" - tcp_socket.send(msg_mode.encode()) - data = tcp_socket.recv(16 * 1024) - if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode - msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" - tcp_socket.send(msg_rgb.encode()) - data = tcp_socket.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) - r = hex_rgb[:2] - if r == " ": - r = "00" - g = hex_rgb[3:4] - if g == " ": - g = "00" - b = hex_rgb[-2:] - if b == " ": - b = "00" - state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) - state["colormode"] = "xy" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode + c = YeelightConnection(ip, light) + Connections[ip] = c + c.enableMusic(host_ip) + +def disableMusic(ip): + if ip in Connections: # Else? LOL + Connections[ip].disableMusic() + +class YeelightConnection(object): + _music = False + _connected = False + _socket = None + _host_ip = "" + + def __init__(self, ip, light): + self._ip = ip + self._light = light + + def connect(self, simple = False): #Use simple when you don't need to reconnect music mode + self.disconnect() #To clean old socket + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(5) + self._socket.connect((self._ip, int(55443))) + if not simple and self._music: + self.enableMusic(self._host_ip) + else: + self._connected = True + + def disconnect(self): + self._connected = False + if self._socket: + self._socket.close() + self._socket = None + + def enableMusic(self, host_ip): + if self._connected and self._music: + raise AssertionError("Already in music mode!") + + self._host_ip = host_ip + + tempSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #Setup listener + tempSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + tempSock.settimeout(5) + + tempSock.bind(("", 0)) + port = tempSock.getsockname()[1] #Get listener port + + tempSock.listen(3) + + if not self._connected: + self.connect(True) #Basic connect for set_music + + self.command("set_music", [1, host_ip, port]) #MAGIC + self.disconnect() #Disconnect from basic mode + + while 1: + try: + conn, addr = tempSock.accept() + if addr[0] == self._ip: #Ignore wrong connections + tempSock.close() #Close listener + self._socket = conn #Replace socket with music one + self._connected = True + self._music = True + break + else: + pass + except Exception as e: + tempSock.close() + raise ConnectionError("Yeelight with IP %s doesn't want to connect in music mode: %s", self._ip, e) + + logging.info("Yeelight device with IP %s is now in music mode", self._ip) + + def disableMusic(self): + if not self._music: + return + + if self._socket: + self._socket.close() + self._socket = None + self._music = False + logging.info("Yeelight device with IP %s is no longer using music mode", self._ip) + + def send(self, data: bytes, flags: int = 0): + try: + if not self._connected: + self.connect() + self._socket.send(data, flags) + except Exception as e: + self._connected = False + raise e + + def recv(self, bufsize: int, flags: int = 0) -> bytes: + try: + if not self._connected: + self.connect() + return self._socket.recv(bufsize, flags) + except Exception as e: + self._connected = False + raise e + + def command(self, api_method, param): + try: + msg = json.dumps({"id": 1, "method": api_method, "params": param}) + "\r\n" + self.send(msg.encode()) + except Exception as e: + logging.warning("Yeelight command error: %s", e) + + def get_state(self): + #logging.info("name is: " + self._light["name"]) + #if self._light["name"].find("desklamp") > 0: logging.info("is desk lamp") + state = {} + msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" + self.send(msg.encode()) + data = self.recv(16 * 1024) + light_data = json.loads(data[:-2].decode("utf8"))["result"] + if light_data[0] == "on": #powerstate + state['on'] = True + else: + state['on'] = False + state["bri"] = int(int(light_data[1]) * 2.54) + #if ip[:-3] == "201" or ip[:-3] == "202": + if self._light["name"].find("desklamp") > 0: msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - tcp_socket.send(msg_ct.encode()) - data = tcp_socket.recv(16 * 1024) - state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + self.send(msg_ct.encode()) + data = self.recv(16 * 1024) + tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + if tempval > 369: tempval = 369 + state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) state["colormode"] = "ct" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode - msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" - tcp_socket.send(msg_hsv.encode()) - data = tcp_socket.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - state["hue"] = int(int(hue_data[0]) * 182) - state["sat"] = int(int(hue_data[1]) * 2.54) - state["colormode"] = "hs" - tcp_socket.close() - return state + else: + msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" + self.send(msg_mode.encode()) + data = self.recv(16 * 1024) + if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode + msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" + self.send(msg_rgb.encode()) + data = self.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) + r = hex_rgb[:2] + if r == " ": + r = "00" + g = hex_rgb[3:4] + if g == " ": + g = "00" + b = hex_rgb[-2:] + if b == " ": + b = "00" + state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) + state["colormode"] = "xy" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode + msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" + self.send(msg_ct.encode()) + data = self.recv(16 * 1024) + state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + state["colormode"] = "ct" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode + msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" + self.send(msg_hsv.encode()) + data = self.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + state["hue"] = int(int(hue_data[0]) * 182) + state["sat"] = int(int(hue_data[1]) * 2.54) + state["colormode"] = "hs" + return state + From b4061f53a8710d88f7174c72ed4c0a798b7fa4f6 Mon Sep 17 00:00:00 2001 From: Silve Date: Fri, 17 Jan 2020 17:07:30 +0300 Subject: [PATCH 02/13] Fixed small string format error --- BridgeEmulator/protocols/yeelight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index 8e4a5e870..c4f00382a 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -205,7 +205,7 @@ def enableMusic(self, host_ip): pass except Exception as e: tempSock.close() - raise ConnectionError("Yeelight with IP %s doesn't want to connect in music mode: %s", self._ip, e) + raise ConnectionError("Yeelight with IP {} doesn't want to connect in music mode: {}".format(self._ip, e)) logging.info("Yeelight device with IP %s is now in music mode", self._ip) From 5bc898be76b6866e75ad65997c3ae7e75083c65b Mon Sep 17 00:00:00 2001 From: alexyao2015 <33379584+alexyao2015@users.noreply.github.com> Date: Fri, 17 Jan 2020 23:58:13 -0500 Subject: [PATCH 03/13] Merge Yeelight Music Mode (#330) * Implementation of Yeelight Music Mode Yeelight now supports Philips Entertainment API * Changed entertainment service method to pass Host IP argument; * Changed sendLightRequest method to pass entertainment Host IP to turn on music mode on yeelight devices before sending them any commands; + Implemented YeelightConnection class which is now used to communicate with Yeelight devices and to maintain Yeelight Music Mode connections; + Implemented dictionary logic to support dedicated yeelight connections; * Fixed small string format error --- BridgeEmulator/HueEmulator3.py | 2 +- BridgeEmulator/functions/entertainment.py | 14 +- BridgeEmulator/functions/lightRequest.py | 4 +- BridgeEmulator/protocols/yeelight.py | 264 ++++++++++++++++------ 4 files changed, 207 insertions(+), 77 deletions(-) diff --git a/BridgeEmulator/HueEmulator3.py b/BridgeEmulator/HueEmulator3.py index c5558d712..1760676a8 100755 --- a/BridgeEmulator/HueEmulator3.py +++ b/BridgeEmulator/HueEmulator3.py @@ -1940,7 +1940,7 @@ def run(https, server_class=ThreadingSimpleServer, handler_class=S): Thread(target=ssdpBroadcast, args=[HOST_IP, HOST_HTTP_PORT, mac]).start() Thread(target=schedulerProcessor).start() Thread(target=syncWithLights, args=[bridge_config["lights"], bridge_config["lights_address"], bridge_config["config"]["whitelist"], bridge_config["groups"], off_if_unreachable]).start() - Thread(target=entertainmentService, args=[bridge_config["lights"], bridge_config["lights_address"], bridge_config["groups"]]).start() + Thread(target=entertainmentService, args=[bridge_config["lights"], bridge_config["lights_address"], bridge_config["groups"], HOST_IP]).start() Thread(target=run, args=[False]).start() if not args.no_serve_https: Thread(target=run, args=[True]).start() diff --git a/BridgeEmulator/functions/entertainment.py b/BridgeEmulator/functions/entertainment.py index 237f1bd75..ce04a7242 100644 --- a/BridgeEmulator/functions/entertainment.py +++ b/BridgeEmulator/functions/entertainment.py @@ -3,7 +3,7 @@ from functions.colors import convert_rgb_xy, convert_xy from functions.lightRequest import sendLightRequest -def entertainmentService(lights, addresses, groups): +def entertainmentService(lights, addresses, groups, host_ip): serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) serverSocket.settimeout(3) #Set a packet timeout that we catch later serverSocket.bind(('127.0.0.1', 2101)) @@ -45,20 +45,20 @@ def entertainmentService(lights, addresses, groups): if fremeID == 24: # => every seconds, increase in case the destination device is overloaded if r == 0 and g == 0 and b == 0: if lightStatus[lightId]["on"]: - sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, host_ip) lightStatus[lightId]["on"] = False elif lightStatus[lightId]["on"] == False: - sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, host_ip) lightStatus[lightId]["on"] = True elif abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) > 50: # to optimize, send brightness only of difference is bigger than this value - sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 3}, lights, addresses, host_ip) lightStatus[lightId]["bri"] = int((r + b + g) / 3) else: - sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses) + sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, host_ip) fremeID += 1 if fremeID == 25: fremeID = 0 - i = i + 9 + i = i + 9 #TODO CHECK BOTTOM SEGMENT, THERE IS NO "i" INCREMENTATION elif data[14] == 1: #cie colorspace i = 16 while i < len(data): @@ -87,7 +87,7 @@ def entertainmentService(lights, addresses, groups): else: fremeID += 1 if fremeID == 24 : #24 = every seconds, increase in case the destination device is overloaded - sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses) + sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, host_ip) fremeID = 0 if len(nativeLights) is not 0: for ip in nativeLights.keys(): diff --git a/BridgeEmulator/functions/lightRequest.py b/BridgeEmulator/functions/lightRequest.py index 64ac43843..701f94a53 100755 --- a/BridgeEmulator/functions/lightRequest.py +++ b/BridgeEmulator/functions/lightRequest.py @@ -7,13 +7,15 @@ from time import sleep from functions.updateGroup import updateGroupStats -def sendLightRequest(light, data, lights, addresses): +def sendLightRequest(light, data, lights, addresses, entertainmentHostIP = None): payload = {} if light in addresses: protocol_name = addresses[light]["protocol"] for protocol in protocols: if "protocols." + protocol_name == protocol.__name__: try: + if entertainmentHostIP and protocol_name == "yeelight": + protocol.enableMusic(addresses[light]["ip"], entertainmentHostIP) light_state = protocol.set_light(addresses[light], lights[light], data) except Exception as e: lights[light]["state"]["reachable"] = False diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index c4bec757d..c4f00382a 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -7,6 +7,8 @@ from functions import light_types, nextFreeId from functions.colors import convert_rgb_xy, convert_xy +Connections = {} + def discover(bridge_config, new_lights): group = ("239.255.255.250", 1982) message = "\r\n".join([ @@ -66,17 +68,17 @@ def discover(bridge_config, new_lights): sock.close() break -def command(ip, api_method, param): +def command(ip, light, api_method, param): + if ip in Connections: + c = Connections[ip] + else: + c = YeelightConnection(ip, light) + Connections[ip] = c try: - tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - tcp_socket.settimeout(5) - tcp_socket.connect((ip, int(55443))) - msg = json.dumps({"id": 1, "method": api_method, "params": param}) + "\r\n" - tcp_socket.send(msg.encode()) - tcp_socket.close() - except Exception as e: - logging.warning("Yeelight command error: %s", e) - + c.command(api_method, param) + finally: + if not c._music and c._connected: + c.disconnect() def set_light(address, light, data): method = 'TCP' @@ -111,67 +113,193 @@ def set_light(address, light, data): # see page 9 http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf # check if hue wants to change brightness for key, value in payload.items(): - command(address["ip"], key, value) + command(address["ip"], light, key, value) def get_light_state(address, light): - #logging.info("name is: " + light["name"]) - #if light["name"].find("desklamp") > 0: logging.info("is desk lamp") - state = {} - tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - tcp_socket.settimeout(5) - tcp_socket.connect((address["ip"], int(55443))) - msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" - tcp_socket.send(msg.encode()) - data = tcp_socket.recv(16 * 1024) - light_data = json.loads(data[:-2].decode("utf8"))["result"] - if light_data[0] == "on": #powerstate - state['on'] = True + ip = address["ip"] + if ip in Connections: + c = Connections[ip] else: - state['on'] = False - state["bri"] = int(int(light_data[1]) * 2.54) - #if ip[:-3] == "201" or ip[:-3] == "202": - if light["name"].find("desklamp") > 0: - msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - tcp_socket.send(msg_ct.encode()) - data = tcp_socket.recv(16 * 1024) - tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - if tempval > 369: tempval = 369 - state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - state["colormode"] = "ct" + c = YeelightConnection(ip, light) + Connections[ip] = c + try: + state = Connections[ip].get_state() + if not c._music and c._connected: + c.disconnect() + return state + finally: + if not c._music and c._connected: + c.disconnect() + +def enableMusic(ip, host_ip): + if ip in Connections: + c = Connections[ip] + if not c._music: + c.enableMusic(host_ip) else: - msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" - tcp_socket.send(msg_mode.encode()) - data = tcp_socket.recv(16 * 1024) - if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode - msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" - tcp_socket.send(msg_rgb.encode()) - data = tcp_socket.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) - r = hex_rgb[:2] - if r == " ": - r = "00" - g = hex_rgb[3:4] - if g == " ": - g = "00" - b = hex_rgb[-2:] - if b == " ": - b = "00" - state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) - state["colormode"] = "xy" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode + c = YeelightConnection(ip, light) + Connections[ip] = c + c.enableMusic(host_ip) + +def disableMusic(ip): + if ip in Connections: # Else? LOL + Connections[ip].disableMusic() + +class YeelightConnection(object): + _music = False + _connected = False + _socket = None + _host_ip = "" + + def __init__(self, ip, light): + self._ip = ip + self._light = light + + def connect(self, simple = False): #Use simple when you don't need to reconnect music mode + self.disconnect() #To clean old socket + self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self._socket.settimeout(5) + self._socket.connect((self._ip, int(55443))) + if not simple and self._music: + self.enableMusic(self._host_ip) + else: + self._connected = True + + def disconnect(self): + self._connected = False + if self._socket: + self._socket.close() + self._socket = None + + def enableMusic(self, host_ip): + if self._connected and self._music: + raise AssertionError("Already in music mode!") + + self._host_ip = host_ip + + tempSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #Setup listener + tempSock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + tempSock.settimeout(5) + + tempSock.bind(("", 0)) + port = tempSock.getsockname()[1] #Get listener port + + tempSock.listen(3) + + if not self._connected: + self.connect(True) #Basic connect for set_music + + self.command("set_music", [1, host_ip, port]) #MAGIC + self.disconnect() #Disconnect from basic mode + + while 1: + try: + conn, addr = tempSock.accept() + if addr[0] == self._ip: #Ignore wrong connections + tempSock.close() #Close listener + self._socket = conn #Replace socket with music one + self._connected = True + self._music = True + break + else: + pass + except Exception as e: + tempSock.close() + raise ConnectionError("Yeelight with IP {} doesn't want to connect in music mode: {}".format(self._ip, e)) + + logging.info("Yeelight device with IP %s is now in music mode", self._ip) + + def disableMusic(self): + if not self._music: + return + + if self._socket: + self._socket.close() + self._socket = None + self._music = False + logging.info("Yeelight device with IP %s is no longer using music mode", self._ip) + + def send(self, data: bytes, flags: int = 0): + try: + if not self._connected: + self.connect() + self._socket.send(data, flags) + except Exception as e: + self._connected = False + raise e + + def recv(self, bufsize: int, flags: int = 0) -> bytes: + try: + if not self._connected: + self.connect() + return self._socket.recv(bufsize, flags) + except Exception as e: + self._connected = False + raise e + + def command(self, api_method, param): + try: + msg = json.dumps({"id": 1, "method": api_method, "params": param}) + "\r\n" + self.send(msg.encode()) + except Exception as e: + logging.warning("Yeelight command error: %s", e) + + def get_state(self): + #logging.info("name is: " + self._light["name"]) + #if self._light["name"].find("desklamp") > 0: logging.info("is desk lamp") + state = {} + msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" + self.send(msg.encode()) + data = self.recv(16 * 1024) + light_data = json.loads(data[:-2].decode("utf8"))["result"] + if light_data[0] == "on": #powerstate + state['on'] = True + else: + state['on'] = False + state["bri"] = int(int(light_data[1]) * 2.54) + #if ip[:-3] == "201" or ip[:-3] == "202": + if self._light["name"].find("desklamp") > 0: msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - tcp_socket.send(msg_ct.encode()) - data = tcp_socket.recv(16 * 1024) - state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + self.send(msg_ct.encode()) + data = self.recv(16 * 1024) + tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + if tempval > 369: tempval = 369 + state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) state["colormode"] = "ct" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode - msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" - tcp_socket.send(msg_hsv.encode()) - data = tcp_socket.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - state["hue"] = int(int(hue_data[0]) * 182) - state["sat"] = int(int(hue_data[1]) * 2.54) - state["colormode"] = "hs" - tcp_socket.close() - return state + else: + msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" + self.send(msg_mode.encode()) + data = self.recv(16 * 1024) + if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode + msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" + self.send(msg_rgb.encode()) + data = self.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) + r = hex_rgb[:2] + if r == " ": + r = "00" + g = hex_rgb[3:4] + if g == " ": + g = "00" + b = hex_rgb[-2:] + if b == " ": + b = "00" + state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) + state["colormode"] = "xy" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode + msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" + self.send(msg_ct.encode()) + data = self.recv(16 * 1024) + state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + state["colormode"] = "ct" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode + msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" + self.send(msg_hsv.encode()) + data = self.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + state["hue"] = int(int(hue_data[0]) * 182) + state["sat"] = int(int(hue_data[1]) * 2.54) + state["colormode"] = "hs" + return state + From 636bc26e0865601522076d0c49ef4347396e793c Mon Sep 17 00:00:00 2001 From: Silve Date: Sat, 18 Jan 2020 10:52:21 +0300 Subject: [PATCH 04/13] Small music mode listener improvement --- BridgeEmulator/protocols/yeelight.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index c4f00382a..fdf46410d 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -202,7 +202,11 @@ def enableMusic(self, host_ip): self._music = True break else: - pass + try: + logging.info("Rejecting connection to the music mode listener from %s", self._ip) + conn.close() + except: + pass except Exception as e: tempSock.close() raise ConnectionError("Yeelight with IP {} doesn't want to connect in music mode: {}".format(self._ip, e)) From ec935c7612398df1d762a2f2e7cb2f839594e59e Mon Sep 17 00:00:00 2001 From: SilveIT Date: Sat, 18 Jan 2020 19:16:25 +0300 Subject: [PATCH 05/13] Small improvements (#332) * Implementation of Yeelight Music Mode Yeelight now supports Philips Entertainment API * Changed entertainment service method to pass Host IP argument; * Changed sendLightRequest method to pass entertainment Host IP to turn on music mode on yeelight devices before sending them any commands; + Implemented YeelightConnection class which is now used to communicate with Yeelight devices and to maintain Yeelight Music Mode connections; + Implemented dictionary logic to support dedicated yeelight connections; * Fixed small string format error * Small music mode listener improvement --- BridgeEmulator/protocols/yeelight.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index c4f00382a..fdf46410d 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -202,7 +202,11 @@ def enableMusic(self, host_ip): self._music = True break else: - pass + try: + logging.info("Rejecting connection to the music mode listener from %s", self._ip) + conn.close() + except: + pass except Exception as e: tempSock.close() raise ConnectionError("Yeelight with IP {} doesn't want to connect in music mode: {}".format(self._ip, e)) From 986cc8eb1739cc2280d207d7e04189c0b153b504 Mon Sep 17 00:00:00 2001 From: Silve Date: Sun, 19 Jan 2020 17:26:12 +0300 Subject: [PATCH 06/13] syncWithLights timedelta fix and Yeelight's get_state rollback --- BridgeEmulator/functions/lightRequest.py | 13 ++- BridgeEmulator/protocols/yeelight.py | 142 ++++++++++------------- 2 files changed, 73 insertions(+), 82 deletions(-) diff --git a/BridgeEmulator/functions/lightRequest.py b/BridgeEmulator/functions/lightRequest.py index 701f94a53..1b5739cae 100755 --- a/BridgeEmulator/functions/lightRequest.py +++ b/BridgeEmulator/functions/lightRequest.py @@ -3,7 +3,7 @@ from functions.colors import convert_rgb_xy, convert_xy from subprocess import check_output from protocols import protocols -from datetime import datetime +from datetime import datetime, timedelta from time import sleep from functions.updateGroup import updateGroupStats @@ -301,8 +301,13 @@ def syncWithLights(lights, addresses, users, groups, off_if_unreachable): #updat i = 0 while i < 300: #sync with lights every 300 seconds or instant if one user is connected for user in users.keys(): - if users[user]["last use date"] == datetime.now().strftime("%Y-%m-%dT%H:%M:%S"): - i = 300 - break + lu = users[user]["last use date"] + try: #in case if last use is not a proper datetime + lu = datetime.strptime(lu, "%Y-%m-%dT%H:%M:%S") + if abs(datetime.now() - lu) <= timedelta(seconds = 2): + i = 300 + break + except: + pass i += 1 sleep(1) diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index fdf46410d..fab8a7482 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -72,7 +72,7 @@ def command(ip, light, api_method, param): if ip in Connections: c = Connections[ip] else: - c = YeelightConnection(ip, light) + c = YeelightConnection(ip) Connections[ip] = c try: c.command(api_method, param) @@ -116,20 +116,67 @@ def set_light(address, light, data): command(address["ip"], light, key, value) def get_light_state(address, light): - ip = address["ip"] - if ip in Connections: - c = Connections[ip] + #logging.info("name is: " + light["name"]) + #if light["name"].find("desklamp") > 0: logging.info("is desk lamp") + state = {} + tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + tcp_socket.settimeout(5) + tcp_socket.connect((address["ip"], int(55443))) + msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" + tcp_socket.send(msg.encode()) + data = tcp_socket.recv(16 * 1024) + light_data = json.loads(data[:-2].decode("utf8"))["result"] + if light_data[0] == "on": #powerstate + state['on'] = True else: - c = YeelightConnection(ip, light) - Connections[ip] = c - try: - state = Connections[ip].get_state() - if not c._music and c._connected: - c.disconnect() - return state - finally: - if not c._music and c._connected: - c.disconnect() + state['on'] = False + state["bri"] = int(int(light_data[1]) * 2.54) + #if ip[:-3] == "201" or ip[:-3] == "202": + if light["name"].find("desklamp") > 0: + msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" + tcp_socket.send(msg_ct.encode()) + data = tcp_socket.recv(16 * 1024) + tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + if tempval > 369: tempval = 369 + state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + state["colormode"] = "ct" + else: + msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" + tcp_socket.send(msg_mode.encode()) + data = tcp_socket.recv(16 * 1024) + if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode + msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" + tcp_socket.send(msg_rgb.encode()) + data = tcp_socket.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) + r = hex_rgb[:2] + if r == " ": + r = "00" + g = hex_rgb[3:4] + if g == " ": + g = "00" + b = hex_rgb[-2:] + if b == " ": + b = "00" + state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) + state["colormode"] = "xy" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode + msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" + tcp_socket.send(msg_ct.encode()) + data = tcp_socket.recv(16 * 1024) + state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + state["colormode"] = "ct" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode + msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" + tcp_socket.send(msg_hsv.encode()) + data = tcp_socket.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + state["hue"] = int(int(hue_data[0]) * 182) + state["sat"] = int(int(hue_data[1]) * 2.54) + state["colormode"] = "hs" + tcp_socket.close() + return state def enableMusic(ip, host_ip): if ip in Connections: @@ -137,7 +184,7 @@ def enableMusic(ip, host_ip): if not c._music: c.enableMusic(host_ip) else: - c = YeelightConnection(ip, light) + c = YeelightConnection(ip) Connections[ip] = c c.enableMusic(host_ip) @@ -151,9 +198,8 @@ class YeelightConnection(object): _socket = None _host_ip = "" - def __init__(self, ip, light): + def __init__(self, ip): self._ip = ip - self._light = light def connect(self, simple = False): #Use simple when you don't need to reconnect music mode self.disconnect() #To clean old socket @@ -246,64 +292,4 @@ def command(self, api_method, param): msg = json.dumps({"id": 1, "method": api_method, "params": param}) + "\r\n" self.send(msg.encode()) except Exception as e: - logging.warning("Yeelight command error: %s", e) - - def get_state(self): - #logging.info("name is: " + self._light["name"]) - #if self._light["name"].find("desklamp") > 0: logging.info("is desk lamp") - state = {} - msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" - self.send(msg.encode()) - data = self.recv(16 * 1024) - light_data = json.loads(data[:-2].decode("utf8"))["result"] - if light_data[0] == "on": #powerstate - state['on'] = True - else: - state['on'] = False - state["bri"] = int(int(light_data[1]) * 2.54) - #if ip[:-3] == "201" or ip[:-3] == "202": - if self._light["name"].find("desklamp") > 0: - msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - self.send(msg_ct.encode()) - data = self.recv(16 * 1024) - tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - if tempval > 369: tempval = 369 - state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - state["colormode"] = "ct" - else: - msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" - self.send(msg_mode.encode()) - data = self.recv(16 * 1024) - if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode - msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" - self.send(msg_rgb.encode()) - data = self.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) - r = hex_rgb[:2] - if r == " ": - r = "00" - g = hex_rgb[3:4] - if g == " ": - g = "00" - b = hex_rgb[-2:] - if b == " ": - b = "00" - state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) - state["colormode"] = "xy" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode - msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - self.send(msg_ct.encode()) - data = self.recv(16 * 1024) - state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - state["colormode"] = "ct" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode - msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" - self.send(msg_hsv.encode()) - data = self.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - state["hue"] = int(int(hue_data[0]) * 182) - state["sat"] = int(int(hue_data[1]) * 2.54) - state["colormode"] = "hs" - return state - + logging.warning("Yeelight command error: %s", e) \ No newline at end of file From 1d83de2f41dd4c37488796ccc656a062395b60b4 Mon Sep 17 00:00:00 2001 From: Silve Date: Sun, 19 Jan 2020 17:28:18 +0300 Subject: [PATCH 07/13] Handle an exception in getIpAddress method --- BridgeEmulator/functions/network_OpenWrt.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/BridgeEmulator/functions/network_OpenWrt.py b/BridgeEmulator/functions/network_OpenWrt.py index 7310953a9..749280a85 100644 --- a/BridgeEmulator/functions/network_OpenWrt.py +++ b/BridgeEmulator/functions/network_OpenWrt.py @@ -11,9 +11,16 @@ def get_interface_ip(ifname): bytes(ifname[:15], 'utf-8')))[20:24]) def getIpAddress(): - ip = socket.gethostbyname(socket.gethostname()) - if ip.startswith("127.") and os.name != "nt": + ip = None + + try: + ip = socket.gethostbyname(socket.gethostname()) + except: + pass + + if (not ip or ip.startswith("127.")) and os.name != "nt": interfaces = [ + "br0", "br-lan", "eth0", "eth1", From 79707d5707b4b738ff321470f3cdd5b733c8ad0a Mon Sep 17 00:00:00 2001 From: SilveIT Date: Mon, 20 Jan 2020 22:04:21 +0300 Subject: [PATCH 08/13] Sync and getIpAddress fixes (#334) * Implementation of Yeelight Music Mode Yeelight now supports Philips Entertainment API * Changed entertainment service method to pass Host IP argument; * Changed sendLightRequest method to pass entertainment Host IP to turn on music mode on yeelight devices before sending them any commands; + Implemented YeelightConnection class which is now used to communicate with Yeelight devices and to maintain Yeelight Music Mode connections; + Implemented dictionary logic to support dedicated yeelight connections; * Fixed small string format error * Small music mode listener improvement * syncWithLights timedelta fix and Yeelight's get_state rollback * Handle an exception in getIpAddress method --- BridgeEmulator/functions/lightRequest.py | 13 +- BridgeEmulator/functions/network_OpenWrt.py | 11 +- BridgeEmulator/protocols/yeelight.py | 142 +++++++++----------- 3 files changed, 82 insertions(+), 84 deletions(-) diff --git a/BridgeEmulator/functions/lightRequest.py b/BridgeEmulator/functions/lightRequest.py index 701f94a53..1b5739cae 100755 --- a/BridgeEmulator/functions/lightRequest.py +++ b/BridgeEmulator/functions/lightRequest.py @@ -3,7 +3,7 @@ from functions.colors import convert_rgb_xy, convert_xy from subprocess import check_output from protocols import protocols -from datetime import datetime +from datetime import datetime, timedelta from time import sleep from functions.updateGroup import updateGroupStats @@ -301,8 +301,13 @@ def syncWithLights(lights, addresses, users, groups, off_if_unreachable): #updat i = 0 while i < 300: #sync with lights every 300 seconds or instant if one user is connected for user in users.keys(): - if users[user]["last use date"] == datetime.now().strftime("%Y-%m-%dT%H:%M:%S"): - i = 300 - break + lu = users[user]["last use date"] + try: #in case if last use is not a proper datetime + lu = datetime.strptime(lu, "%Y-%m-%dT%H:%M:%S") + if abs(datetime.now() - lu) <= timedelta(seconds = 2): + i = 300 + break + except: + pass i += 1 sleep(1) diff --git a/BridgeEmulator/functions/network_OpenWrt.py b/BridgeEmulator/functions/network_OpenWrt.py index 7310953a9..749280a85 100644 --- a/BridgeEmulator/functions/network_OpenWrt.py +++ b/BridgeEmulator/functions/network_OpenWrt.py @@ -11,9 +11,16 @@ def get_interface_ip(ifname): bytes(ifname[:15], 'utf-8')))[20:24]) def getIpAddress(): - ip = socket.gethostbyname(socket.gethostname()) - if ip.startswith("127.") and os.name != "nt": + ip = None + + try: + ip = socket.gethostbyname(socket.gethostname()) + except: + pass + + if (not ip or ip.startswith("127.")) and os.name != "nt": interfaces = [ + "br0", "br-lan", "eth0", "eth1", diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index fdf46410d..fab8a7482 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -72,7 +72,7 @@ def command(ip, light, api_method, param): if ip in Connections: c = Connections[ip] else: - c = YeelightConnection(ip, light) + c = YeelightConnection(ip) Connections[ip] = c try: c.command(api_method, param) @@ -116,20 +116,67 @@ def set_light(address, light, data): command(address["ip"], light, key, value) def get_light_state(address, light): - ip = address["ip"] - if ip in Connections: - c = Connections[ip] + #logging.info("name is: " + light["name"]) + #if light["name"].find("desklamp") > 0: logging.info("is desk lamp") + state = {} + tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + tcp_socket.settimeout(5) + tcp_socket.connect((address["ip"], int(55443))) + msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" + tcp_socket.send(msg.encode()) + data = tcp_socket.recv(16 * 1024) + light_data = json.loads(data[:-2].decode("utf8"))["result"] + if light_data[0] == "on": #powerstate + state['on'] = True else: - c = YeelightConnection(ip, light) - Connections[ip] = c - try: - state = Connections[ip].get_state() - if not c._music and c._connected: - c.disconnect() - return state - finally: - if not c._music and c._connected: - c.disconnect() + state['on'] = False + state["bri"] = int(int(light_data[1]) * 2.54) + #if ip[:-3] == "201" or ip[:-3] == "202": + if light["name"].find("desklamp") > 0: + msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" + tcp_socket.send(msg_ct.encode()) + data = tcp_socket.recv(16 * 1024) + tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + if tempval > 369: tempval = 369 + state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + state["colormode"] = "ct" + else: + msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" + tcp_socket.send(msg_mode.encode()) + data = tcp_socket.recv(16 * 1024) + if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode + msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" + tcp_socket.send(msg_rgb.encode()) + data = tcp_socket.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) + r = hex_rgb[:2] + if r == " ": + r = "00" + g = hex_rgb[3:4] + if g == " ": + g = "00" + b = hex_rgb[-2:] + if b == " ": + b = "00" + state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) + state["colormode"] = "xy" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode + msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" + tcp_socket.send(msg_ct.encode()) + data = tcp_socket.recv(16 * 1024) + state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) + state["colormode"] = "ct" + elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode + msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" + tcp_socket.send(msg_hsv.encode()) + data = tcp_socket.recv(16 * 1024) + hue_data = json.loads(data[:-2].decode("utf8"))["result"] + state["hue"] = int(int(hue_data[0]) * 182) + state["sat"] = int(int(hue_data[1]) * 2.54) + state["colormode"] = "hs" + tcp_socket.close() + return state def enableMusic(ip, host_ip): if ip in Connections: @@ -137,7 +184,7 @@ def enableMusic(ip, host_ip): if not c._music: c.enableMusic(host_ip) else: - c = YeelightConnection(ip, light) + c = YeelightConnection(ip) Connections[ip] = c c.enableMusic(host_ip) @@ -151,9 +198,8 @@ class YeelightConnection(object): _socket = None _host_ip = "" - def __init__(self, ip, light): + def __init__(self, ip): self._ip = ip - self._light = light def connect(self, simple = False): #Use simple when you don't need to reconnect music mode self.disconnect() #To clean old socket @@ -246,64 +292,4 @@ def command(self, api_method, param): msg = json.dumps({"id": 1, "method": api_method, "params": param}) + "\r\n" self.send(msg.encode()) except Exception as e: - logging.warning("Yeelight command error: %s", e) - - def get_state(self): - #logging.info("name is: " + self._light["name"]) - #if self._light["name"].find("desklamp") > 0: logging.info("is desk lamp") - state = {} - msg=json.dumps({"id": 1, "method": "get_prop", "params":["power","bright"]}) + "\r\n" - self.send(msg.encode()) - data = self.recv(16 * 1024) - light_data = json.loads(data[:-2].decode("utf8"))["result"] - if light_data[0] == "on": #powerstate - state['on'] = True - else: - state['on'] = False - state["bri"] = int(int(light_data[1]) * 2.54) - #if ip[:-3] == "201" or ip[:-3] == "202": - if self._light["name"].find("desklamp") > 0: - msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - self.send(msg_ct.encode()) - data = self.recv(16 * 1024) - tempval = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - if tempval > 369: tempval = 369 - state["ct"] = tempval # int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - state["colormode"] = "ct" - else: - msg_mode=json.dumps({"id": 1, "method": "get_prop", "params":["color_mode"]}) + "\r\n" - self.send(msg_mode.encode()) - data = self.recv(16 * 1024) - if json.loads(data[:-2].decode("utf8"))["result"][0] == "1": #rgb mode - msg_rgb=json.dumps({"id": 1, "method": "get_prop", "params":["rgb"]}) + "\r\n" - self.send(msg_rgb.encode()) - data = self.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - hex_rgb = "%6x" % int(json.loads(data[:-2].decode("utf8"))["result"][0]) - r = hex_rgb[:2] - if r == " ": - r = "00" - g = hex_rgb[3:4] - if g == " ": - g = "00" - b = hex_rgb[-2:] - if b == " ": - b = "00" - state["xy"] = convert_rgb_xy(int(r,16), int(g,16), int(b,16)) - state["colormode"] = "xy" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "2": #ct mode - msg_ct=json.dumps({"id": 1, "method": "get_prop", "params":["ct"]}) + "\r\n" - self.send(msg_ct.encode()) - data = self.recv(16 * 1024) - state["ct"] = int(-(347/4800) * int(json.loads(data[:-2].decode("utf8"))["result"][0]) +(2989900/4800)) - state["colormode"] = "ct" - elif json.loads(data[:-2].decode("utf8"))["result"][0] == "3": #hs mode - msg_hsv=json.dumps({"id": 1, "method": "get_prop", "params":["hue","sat"]}) + "\r\n" - self.send(msg_hsv.encode()) - data = self.recv(16 * 1024) - hue_data = json.loads(data[:-2].decode("utf8"))["result"] - state["hue"] = int(int(hue_data[0]) * 182) - state["sat"] = int(int(hue_data[1]) * 2.54) - state["colormode"] = "hs" - return state - + logging.warning("Yeelight command error: %s", e) \ No newline at end of file From 28728047f9996a832ec97e65d3951e810a36fe13 Mon Sep 17 00:00:00 2001 From: Silve Date: Thu, 23 Jan 2020 00:55:14 +0300 Subject: [PATCH 09/13] Color brightness fixes + Added RGB argument to sendLightRequest and most of set_light methods to avoid double RGB conversion; * Fixed possibly incorrect brightness value in MiBox, ESPHome protocols; * Transition time in entertainment module is little bit dynamic now; --- BridgeEmulator/functions/colors.py | 19 +++++++-- BridgeEmulator/functions/entertainment.py | 52 ++++++++++++----------- BridgeEmulator/functions/lightRequest.py | 26 +++++++++--- BridgeEmulator/protocols/esphome.py | 9 ++-- BridgeEmulator/protocols/mi_box.py | 9 ++-- BridgeEmulator/protocols/tasmota.py | 9 ++-- BridgeEmulator/protocols/yeelight.py | 12 ++++-- 7 files changed, 88 insertions(+), 48 deletions(-) diff --git a/BridgeEmulator/functions/colors.py b/BridgeEmulator/functions/colors.py index 4889910f6..258e53ead 100644 --- a/BridgeEmulator/functions/colors.py +++ b/BridgeEmulator/functions/colors.py @@ -1,4 +1,16 @@ -def convert_rgb_xy(red,green,blue): +def rgbBrightness(rgb, brightness): + r = sorted((0, int(rgb[0] * brightness) >> 8, 255))[1] #calculate with brightness and clamp + g = sorted((0, int(rgb[1] * brightness) >> 8, 255))[1] + b = sorted((0, int(rgb[2] * brightness) >> 8, 255))[1] + return [r, g, b] + +def clampRGB(rgb): + r = sorted((0, int(rgb[0]), 255))[1] + g = sorted((0, int(rgb[1]), 255))[1] + b = sorted((0, int(rgb[2]), 255))[1] + return [r, g, b] + +def convert_rgb_xy(red, green, blue): red = pow((red + 0.055) / (1.0 + 0.055), 2.4) if red > 0.04045 else red / 12.92 green = pow((green + 0.055) / (1.0 + 0.055), 2.4) if green > 0.04045 else green / 12.92 blue = pow((blue + 0.055) / (1.0 + 0.055), 2.4) if blue > 0.04045 else blue / 12.92 @@ -51,7 +63,7 @@ def convert_xy(x, y, bri): #needed for milight hub that don't work with xy value r = 0 if r < 0 else r g = 0 if g < 0 else g b = 0 if b < 0 else b - return [int(r * bri), int(g * bri), int(b * bri)] + return clampRGB([r * bri, g * bri, b * bri]) def hsv_to_rgb(h, s, v): s = float(s / 254) @@ -84,5 +96,4 @@ def hsv_to_rgb(h, s, v): g = 0 b = x - r, g, b = int(r * 255), int(g * 255), int(b * 255) - return r, g, b + return clampRGB([r * 255, g * 255, b * 255]) diff --git a/BridgeEmulator/functions/entertainment.py b/BridgeEmulator/functions/entertainment.py index ce04a7242..1f7e74463 100644 --- a/BridgeEmulator/functions/entertainment.py +++ b/BridgeEmulator/functions/entertainment.py @@ -7,7 +7,7 @@ def entertainmentService(lights, addresses, groups, host_ip): serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) serverSocket.settimeout(3) #Set a packet timeout that we catch later serverSocket.bind(('127.0.0.1', 2101)) - fremeID = 0 + frameID = 0 lightStatus = {} syncing = False #Flag to check whether or not we had been syncing when a timeout occurs while True: @@ -26,38 +26,45 @@ def entertainmentService(lights, addresses, groups, host_ip): r = int((data[i+3] * 256 + data[i+4]) / 256) g = int((data[i+5] * 256 + data[i+6]) / 256) b = int((data[i+7] * 256 + data[i+7]) / 256) + proto = addresses[str(lightId)]["protocol"] if lightId not in lightStatus: lightStatus[lightId] = {"on": False, "bri": 1} if r == 0 and g == 0 and b == 0: lights[str(lightId)]["state"]["on"] = False else: lights[str(lightId)]["state"].update({"on": True, "bri": int((r + g + b) / 3), "xy": convert_rgb_xy(r, g, b), "colormode": "xy"}) - if addresses[str(lightId)]["protocol"] in ["native", "native_multi", "native_single"]: + if proto in ["native", "native_multi", "native_single"]: if addresses[str(lightId)]["ip"] not in nativeLights: nativeLights[addresses[str(lightId)]["ip"]] = {} nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = [r, g, b] - if addresses[str(lightId)]["protocol"] == "esphome": + if proto == "esphome": if addresses[str(lightId)]["ip"] not in esphomeLights: esphomeLights[addresses[str(lightId)]["ip"]] = {} bri = int(max(r,g,b)) esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] else: - if fremeID == 24: # => every seconds, increase in case the destination device is overloaded - if r == 0 and g == 0 and b == 0: + if frameID == 24: # => every seconds, increase in case the destination device is overloaded + gottaSend = False + yee = proto == "yeelight" + brABS = abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) + if r == 0 and g == 0 and b == 0: #Turn off if color is black if lightStatus[lightId]["on"]: - sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, host_ip) + sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, None, host_ip) lightStatus[lightId]["on"] = False - elif lightStatus[lightId]["on"] == False: - sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, host_ip) - lightStatus[lightId]["on"] = True - elif abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) > 50: # to optimize, send brightness only of difference is bigger than this value - sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 3}, lights, addresses, host_ip) - lightStatus[lightId]["bri"] = int((r + b + g) / 3) else: - sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, host_ip) - fremeID += 1 - if fremeID == 25: - fremeID = 0 + if lightStatus[lightId]["on"] == False: #Turn on if color is not black + sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, None, host_ip) + lightStatus[lightId]["on"] = True + elif brABS > 50: # to optimize, send brightness only if difference is bigger than this value + sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 150 / brABS}, lights, addresses, None, host_ip) + lightStatus[lightId]["bri"] = int((r + b + g) / 3) + else: + gottaSend = True + if gottaSend or yee: + sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, [r, g, b], host_ip) + frameID += 1 + if frameID == 25: + frameID = 0 i = i + 9 #TODO CHECK BOTTOM SEGMENT, THERE IS NO "i" INCREMENTATION elif data[14] == 1: #cie colorspace i = 16 @@ -79,16 +86,13 @@ def entertainmentService(lights, addresses, groups, host_ip): if addresses[str(lightId)]["protocol"] == "esphome": if addresses[str(lightId)]["ip"] not in esphomeLights: esphomeLights[addresses[str(lightId)]["ip"]] = {} - color = convert_xy(x, y, bri) - r = int(color[0]) - g = int(color[1]) - b = int(color[2]) + r, g, b = convert_xy(x, y, bri) esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] else: - fremeID += 1 - if fremeID == 24 : #24 = every seconds, increase in case the destination device is overloaded - sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, host_ip) - fremeID = 0 + frameID += 1 + if frameID == 24 : #24 = every seconds, increase in case the destination device is overloaded + sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, None, host_ip) + frameID = 0 if len(nativeLights) is not 0: for ip in nativeLights.keys(): udpmsg = bytearray() diff --git a/BridgeEmulator/functions/lightRequest.py b/BridgeEmulator/functions/lightRequest.py index 1b5739cae..c4c872284 100755 --- a/BridgeEmulator/functions/lightRequest.py +++ b/BridgeEmulator/functions/lightRequest.py @@ -1,13 +1,13 @@ import logging, json from functions.request import sendRequest -from functions.colors import convert_rgb_xy, convert_xy +from functions.colors import convert_rgb_xy, convert_xy, rgbBrightness from subprocess import check_output from protocols import protocols from datetime import datetime, timedelta from time import sleep from functions.updateGroup import updateGroupStats -def sendLightRequest(light, data, lights, addresses, entertainmentHostIP = None): +def sendLightRequest(light, data, lights, addresses, rgb = None, entertainmentHostIP = None): payload = {} if light in addresses: protocol_name = addresses[light]["protocol"] @@ -16,7 +16,10 @@ def sendLightRequest(light, data, lights, addresses, entertainmentHostIP = None) try: if entertainmentHostIP and protocol_name == "yeelight": protocol.enableMusic(addresses[light]["ip"], entertainmentHostIP) - light_state = protocol.set_light(addresses[light], lights[light], data) + if protocol_name in ["yeelight", "mi_box", "esphome", "tasmota"]: + protocol.set_light(addresses[light], lights[light], data, rgb) + else: + protocol.set_light(addresses[light], lights[light], data) except Exception as e: lights[light]["state"]["reachable"] = False logging.warning(lights[light]["name"] + " light not reachable: %s", e) @@ -72,7 +75,10 @@ def sendLightRequest(light, data, lights, addresses, entertainmentHostIP = None) color_data["t"] = ct255 elif colormode == "xy": color_data["m"] = 3 - (color_data["r"], color_data["g"], color_data["b"]) = convert_xy(xy[0], xy[1], 255) + if rgb: + (color_data["r"], color_data["g"], color_data["b"]) = rgbBrightness(rgb, bri) + else: + (color_data["r"], color_data["g"], color_data["b"]) = convert_xy(xy[0], xy[1], bri) url += "&color="+json.dumps(color_data) url += "&brightness=" + str(round(float(bri)/255*100)) @@ -107,7 +113,10 @@ def sendLightRequest(light, data, lights, addresses, entertainmentHostIP = None) payload["saturation"] = value * 100 / 255 elif key == "xy": payload["color"] = {} - (payload["color"]["r"], payload["color"]["g"], payload["color"]["b"]) = convert_xy(value[0], value[1], lights[light]["state"]["bri"]) + if rgb: + payload["color"]["r"], payload["color"]["g"], payload["color"]["b"] = rgbBrightness(rgb, lights[light]["state"]["bri"]) + else: + payload["color"]["r"], payload["color"]["g"], payload["color"]["b"] = convert_xy(value[0], value[1], lights[light]["state"]["bri"]) logging.info(json.dumps(payload)) elif addresses[light]["protocol"] == "ikea_tradfri": #IKEA Tradfri bulb @@ -166,8 +175,11 @@ def sendLightRequest(light, data, lights, addresses, entertainmentHostIP = None) logging.info(pretty_json(data)) bri = data["bri"] if "bri" in data else lights[light]["state"]["bri"] xy = data["xy"] if "xy" in data else lights[light]["state"]["xy"] - rgb = convert_xy(xy[0], xy[1], bri) - msg = bytearray([0x41, rgb[0], rgb[1], rgb[2], 0x00, 0xf0, 0x0f]) + if rgb: + color = rgbBrightness(rgb, bri) + else: + color = convert_xy(xy[0], xy[1], bri) + msg = bytearray([0x41, color[0], color[1], color[2], 0x00, 0xf0, 0x0f]) checksum = sum(msg) & 0xFF msg.append(checksum) sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP diff --git a/BridgeEmulator/protocols/esphome.py b/BridgeEmulator/protocols/esphome.py index eb6eb9b60..aee48d082 100644 --- a/BridgeEmulator/protocols/esphome.py +++ b/BridgeEmulator/protocols/esphome.py @@ -9,7 +9,7 @@ from time import sleep from subprocess import check_output from functions import light_types, nextFreeId -from functions.colors import convert_rgb_xy, convert_xy, hsv_to_rgb +from functions.colors import convert_rgb_xy, convert_xy, hsv_to_rgb, rgbBrightness from functions.network import getIpAddress def getRequest(address, request_data, timeout=3): @@ -150,7 +150,7 @@ def discover(bridge_config, new_lights): except Exception as e: logging.debug("ESPHome: ip " + ip + " is unknown device, " + str(e)) -def set_light(address, light, data): +def set_light(address, light, data, rgb = None): logging.debug("ESPHome: invoked! IP=" + address["ip"]) logging.debug(light["modelid"]) logging.debug(data) @@ -196,7 +196,10 @@ def set_light(address, light, data): request_data = addRequest(request_data, "brightness", brightness) if address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB", "ESPHome-CT"]: if ("xy" in data) and (address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB"]): - color = convert_xy(data['xy'][0], data['xy'][1], 255) + if rgb: + color = rgbBrightness(rgb, light["state"]["bri"]) + else: + color = convert_xy(data['xy'][0], data['xy'][1], light["state"]["bri"]) request_data = addRequest(request_data, "r", color[0]) request_data = addRequest(request_data, "g", color[1]) request_data = addRequest(request_data, "b", color[2]) diff --git a/BridgeEmulator/protocols/mi_box.py b/BridgeEmulator/protocols/mi_box.py index baa45cf75..710f1aa0c 100644 --- a/BridgeEmulator/protocols/mi_box.py +++ b/BridgeEmulator/protocols/mi_box.py @@ -1,5 +1,5 @@ import logging, binascii, socket, colorsys, time -from functions.colors import convert_rgb_xy, convert_xy +from functions.colors import convert_rgb_xy, convert_xy, rgbBrightness #todo: add support for multiple mi boxes? these globals don't look nice commandCounter = 0 @@ -8,7 +8,7 @@ sock = None lastSentMessageTime = 0 -def set_light(address, light, data): +def set_light(address, light, data, rgb = None): for key, value in data.items(): light["state"][key] = value @@ -18,7 +18,10 @@ def set_light(address, light, data): colormode = light["state"]["colormode"] if colormode == "xy": xy = light["state"]["xy"] - (r,g,b) = convert_xy(xy[0], xy[1], 100.0) + if rgb: + r, g, b = rgbBrightness(rgb, light["state"]["bri"]) + else: + r, g, b = convert_xy(xy[0], xy[1], light["state"]["bri"]) (hue, saturation, value) = colorsys.rgb_to_hsv(r,g,b) sendHueCmd(address, hue*255) sendSaturationCmd(address, (1-saturation)*100) diff --git a/BridgeEmulator/protocols/tasmota.py b/BridgeEmulator/protocols/tasmota.py index fd9b30cdc..7707c2e2c 100755 --- a/BridgeEmulator/protocols/tasmota.py +++ b/BridgeEmulator/protocols/tasmota.py @@ -9,7 +9,7 @@ from time import sleep from subprocess import check_output from functions import light_types, nextFreeId -from functions.colors import convert_rgb_xy, convert_xy +from functions.colors import convert_rgb_xy, convert_xy, rgbBrightness from functions.network import getIpAddress @@ -60,7 +60,7 @@ def discover(bridge_config, new_lights): -def set_light(address, light, data): +def set_light(address, light, data, rgb = None): logging.debug("tasmota: invoked! IP=" + address["ip"]) for key, value in data.items(): @@ -77,7 +77,10 @@ def set_light(address, light, data): elif key == "ct": color = {} elif key == "xy": - color = convert_xy(value[0], value[1], light["state"]["bri"]) + if rgb: + color = rgbBrightness(rgb, light["state"]["bri"]) + else: + color = convert_xy(value[0], value[1], light["state"]["bri"]) sendRequest ("http://"+address["ip"]+"/cm?cmnd=Color%20" + str(color[0]) + "," + str(color[1]) + "," + str(color[2])) elif key == "alert": diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index fab8a7482..9a0051fd4 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -5,7 +5,7 @@ import sys from functions import light_types, nextFreeId -from functions.colors import convert_rgb_xy, convert_xy +from functions.colors import convert_rgb_xy, convert_xy, rgbBrightness Connections = {} @@ -80,12 +80,12 @@ def command(ip, light, api_method, param): if not c._music and c._connected: c.disconnect() -def set_light(address, light, data): +def set_light(address, light, data, rgb = None): method = 'TCP' payload = {} transitiontime = 400 if "transitiontime" in data: - transitiontime = data["transitiontime"] * 100 + transitiontime = int(data["transitiontime"] * 100) for key, value in data.items(): if key == "on": if value: @@ -104,7 +104,11 @@ def set_light(address, light, data): elif key == "sat": payload["set_hsv"] = [int(light["state"]["hue"] / 182), int(value / 2.54), "smooth", transitiontime] elif key == "xy": - color = convert_xy(value[0], value[1], light["state"]["bri"]) + bri = light["state"]["bri"] + if rgb: + color = rgbBrightness(rgb, bri) + else: + color = convert_xy(value[0], value[1], bri) payload["set_rgb"] = [(color[0] * 65536) + (color[1] * 256) + color[2], "smooth", transitiontime] #according to docs, yeelight needs this to set rgb. its r * 65536 + g * 256 + b elif key == "alert" and value != "none": payload["start_cf"] = [ 4, 0, "1000, 2, 5500, 100, 1000, 2, 5500, 1, 1000, 2, 5500, 100, 1000, 2, 5500, 1"] From 19919a538d8dae9464c8494d87226ec89013733a Mon Sep 17 00:00:00 2001 From: Silve Date: Thu, 23 Jan 2020 01:18:40 +0300 Subject: [PATCH 10/13] Fixed weird stuff with tabulation --- BridgeEmulator/functions/entertainment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BridgeEmulator/functions/entertainment.py b/BridgeEmulator/functions/entertainment.py index 56277662c..dbe2f2217 100644 --- a/BridgeEmulator/functions/entertainment.py +++ b/BridgeEmulator/functions/entertainment.py @@ -93,7 +93,7 @@ def entertainmentService(lights, addresses, groups, host_ip): if frameID == 24 : #24 = every seconds, increase in case the destination device is overloaded sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, None, host_ip) frameID = 0 - i = i + 9 + i = i + 9 if len(nativeLights) is not 0: for ip in nativeLights.keys(): udpmsg = bytearray() From 54a9349ffbc2a8198f56020bb3d2ea73aa83f57e Mon Sep 17 00:00:00 2001 From: Silve Date: Thu, 23 Jan 2020 11:35:06 +0300 Subject: [PATCH 11/13] Git's core.autocrlf to false to fix end of the line --- BridgeEmulator/functions/entertainment.py | 234 +++++++++++----------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/BridgeEmulator/functions/entertainment.py b/BridgeEmulator/functions/entertainment.py index dbe2f2217..8131b7f36 100644 --- a/BridgeEmulator/functions/entertainment.py +++ b/BridgeEmulator/functions/entertainment.py @@ -1,117 +1,117 @@ -import socket, logging -from subprocess import Popen -from functions.colors import convert_rgb_xy, convert_xy -from functions.lightRequest import sendLightRequest - -def entertainmentService(lights, addresses, groups, host_ip): - serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - serverSocket.settimeout(3) #Set a packet timeout that we catch later - serverSocket.bind(('127.0.0.1', 2101)) - frameID = 0 - lightStatus = {} - syncing = False #Flag to check whether or not we had been syncing when a timeout occurs - while True: - try: - data = serverSocket.recvfrom(106)[0] - nativeLights = {} - esphomeLights = {} - if data[:9].decode('utf-8') == "HueStream": - syncing = True #Set sync flag when receiving valid data - if data[14] == 0: #rgb colorspace - i = 16 - while i < len(data): - if data[i] == 0: #Type of device 0x00 = Light - lightId = data[i+1] * 256 + data[i+2] - if lightId != 0: - r = int((data[i+3] * 256 + data[i+4]) / 256) - g = int((data[i+5] * 256 + data[i+6]) / 256) - b = int((data[i+7] * 256 + data[i+8]) / 256) - proto = addresses[str(lightId)]["protocol"] - if lightId not in lightStatus: - lightStatus[lightId] = {"on": False, "bri": 1} - if r == 0 and g == 0 and b == 0: - lights[str(lightId)]["state"]["on"] = False - else: - lights[str(lightId)]["state"].update({"on": True, "bri": int((r + g + b) / 3), "xy": convert_rgb_xy(r, g, b), "colormode": "xy"}) - if proto in ["native", "native_multi", "native_single"]: - if addresses[str(lightId)]["ip"] not in nativeLights: - nativeLights[addresses[str(lightId)]["ip"]] = {} - nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = [r, g, b] - if proto == "esphome": - if addresses[str(lightId)]["ip"] not in esphomeLights: - esphomeLights[addresses[str(lightId)]["ip"]] = {} - bri = int(max(r,g,b)) - esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] - else: - if frameID == 24: # => every seconds, increase in case the destination device is overloaded - gottaSend = False - yee = proto == "yeelight" - brABS = abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) - if r == 0 and g == 0 and b == 0: #Turn off if color is black - if lightStatus[lightId]["on"]: - sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, None, host_ip) - lightStatus[lightId]["on"] = False - else: - if lightStatus[lightId]["on"] == False: #Turn on if color is not black - sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, None, host_ip) - lightStatus[lightId]["on"] = True - elif brABS > 50: # to optimize, send brightness only if difference is bigger than this value - sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 150 / brABS}, lights, addresses, None, host_ip) - lightStatus[lightId]["bri"] = int((r + b + g) / 3) - else: - gottaSend = True - if gottaSend or yee: - sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, [r, g, b], host_ip) - frameID += 1 - if frameID == 25: - frameID = 0 - i = i + 9 - elif data[14] == 1: #cie colorspace - i = 16 - while i < len(data): - if data[i] == 0: #Type of device 0x00 = Light - lightId = data[i+1] * 256 + data[i+2] - if lightId != 0: - x = (data[i+3] * 256 + data[i+4]) / 65535 - y = (data[i+5] * 256 + data[i+6]) / 65535 - bri = int((data[i+7] * 256 + data[i+8]) / 256) - if bri == 0: - lights[str(lightId)]["state"]["on"] = False - else: - lights[str(lightId)]["state"].update({"on": True, "bri": bri, "xy": [x,y], "colormode": "xy"}) - if addresses[str(lightId)]["protocol"] in ["native", "native_multi", "native_single"]: - if addresses[str(lightId)]["ip"] not in nativeLights: - nativeLights[addresses[str(lightId)]["ip"]] = {} - nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = convert_xy(x, y, bri) - if addresses[str(lightId)]["protocol"] == "esphome": - if addresses[str(lightId)]["ip"] not in esphomeLights: - esphomeLights[addresses[str(lightId)]["ip"]] = {} - r, g, b = convert_xy(x, y, bri) - esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] - else: - frameID += 1 - if frameID == 24 : #24 = every seconds, increase in case the destination device is overloaded - sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, None, host_ip) - frameID = 0 - i = i + 9 - if len(nativeLights) is not 0: - for ip in nativeLights.keys(): - udpmsg = bytearray() - for light in nativeLights[ip].keys(): - udpmsg += bytes([light]) + bytes([nativeLights[ip][light][0]]) + bytes([nativeLights[ip][light][1]]) + bytes([nativeLights[ip][light][2]]) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP - sock.sendto(udpmsg, (ip.split(":")[0], 2100)) - if len(esphomeLights) is not 0: - for ip in esphomeLights.keys(): - udpmsg = bytearray() - udpmsg += bytes([0]) + bytes([esphomeLights[ip]["color"][0]]) + bytes([esphomeLights[ip]["color"][1]]) + bytes([esphomeLights[ip]["color"][2]]) + bytes([esphomeLights[ip]["color"][3]]) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP - sock.sendto(udpmsg, (ip.split(":")[0], 2100)) - except Exception: #Assuming the only exception is a network timeout, please don't scream at me - if syncing: #Reset sync status and kill relay service - logging.info("Entertainment Service was syncing and has timed out, stopping server and clearing state") - Popen(["killall", "entertain-srv"]) - for group in groups.keys(): - if "type" in groups[group] and groups[group]["type"] == "Entertainment": - groups[group]["stream"].update({"active": False, "owner": None}) - syncing = False +import socket, logging +from subprocess import Popen +from functions.colors import convert_rgb_xy, convert_xy +from functions.lightRequest import sendLightRequest + +def entertainmentService(lights, addresses, groups, host_ip): + serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + serverSocket.settimeout(3) #Set a packet timeout that we catch later + serverSocket.bind(('127.0.0.1', 2101)) + frameID = 0 + lightStatus = {} + syncing = False #Flag to check whether or not we had been syncing when a timeout occurs + while True: + try: + data = serverSocket.recvfrom(106)[0] + nativeLights = {} + esphomeLights = {} + if data[:9].decode('utf-8') == "HueStream": + syncing = True #Set sync flag when receiving valid data + if data[14] == 0: #rgb colorspace + i = 16 + while i < len(data): + if data[i] == 0: #Type of device 0x00 = Light + lightId = data[i+1] * 256 + data[i+2] + if lightId != 0: + r = int((data[i+3] * 256 + data[i+4]) / 256) + g = int((data[i+5] * 256 + data[i+6]) / 256) + b = int((data[i+7] * 256 + data[i+8]) / 256) + proto = addresses[str(lightId)]["protocol"] + if lightId not in lightStatus: + lightStatus[lightId] = {"on": False, "bri": 1} + if r == 0 and g == 0 and b == 0: + lights[str(lightId)]["state"]["on"] = False + else: + lights[str(lightId)]["state"].update({"on": True, "bri": int((r + g + b) / 3), "xy": convert_rgb_xy(r, g, b), "colormode": "xy"}) + if proto in ["native", "native_multi", "native_single"]: + if addresses[str(lightId)]["ip"] not in nativeLights: + nativeLights[addresses[str(lightId)]["ip"]] = {} + nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = [r, g, b] + if proto == "esphome": + if addresses[str(lightId)]["ip"] not in esphomeLights: + esphomeLights[addresses[str(lightId)]["ip"]] = {} + bri = int(max(r,g,b)) + esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] + else: + if frameID == 24: # => every seconds, increase in case the destination device is overloaded + gottaSend = False + yee = proto == "yeelight" + brABS = abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) + if r == 0 and g == 0 and b == 0: #Turn off if color is black + if lightStatus[lightId]["on"]: + sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, None, host_ip) + lightStatus[lightId]["on"] = False + else: + if lightStatus[lightId]["on"] == False: #Turn on if color is not black + sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, None, host_ip) + lightStatus[lightId]["on"] = True + elif brABS > 50: # to optimize, send brightness only if difference is bigger than this value + sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 150 / brABS}, lights, addresses, None, host_ip) + lightStatus[lightId]["bri"] = int((r + b + g) / 3) + else: + gottaSend = True + if gottaSend or yee: + sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, [r, g, b], host_ip) + frameID += 1 + if frameID == 25: + frameID = 0 + i = i + 9 + elif data[14] == 1: #cie colorspace + i = 16 + while i < len(data): + if data[i] == 0: #Type of device 0x00 = Light + lightId = data[i+1] * 256 + data[i+2] + if lightId != 0: + x = (data[i+3] * 256 + data[i+4]) / 65535 + y = (data[i+5] * 256 + data[i+6]) / 65535 + bri = int((data[i+7] * 256 + data[i+8]) / 256) + if bri == 0: + lights[str(lightId)]["state"]["on"] = False + else: + lights[str(lightId)]["state"].update({"on": True, "bri": bri, "xy": [x,y], "colormode": "xy"}) + if addresses[str(lightId)]["protocol"] in ["native", "native_multi", "native_single"]: + if addresses[str(lightId)]["ip"] not in nativeLights: + nativeLights[addresses[str(lightId)]["ip"]] = {} + nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = convert_xy(x, y, bri) + if addresses[str(lightId)]["protocol"] == "esphome": + if addresses[str(lightId)]["ip"] not in esphomeLights: + esphomeLights[addresses[str(lightId)]["ip"]] = {} + r, g, b = convert_xy(x, y, bri) + esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] + else: + frameID += 1 + if frameID == 24 : #24 = every seconds, increase in case the destination device is overloaded + sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, None, host_ip) + frameID = 0 + i = i + 9 + if len(nativeLights) is not 0: + for ip in nativeLights.keys(): + udpmsg = bytearray() + for light in nativeLights[ip].keys(): + udpmsg += bytes([light]) + bytes([nativeLights[ip][light][0]]) + bytes([nativeLights[ip][light][1]]) + bytes([nativeLights[ip][light][2]]) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP + sock.sendto(udpmsg, (ip.split(":")[0], 2100)) + if len(esphomeLights) is not 0: + for ip in esphomeLights.keys(): + udpmsg = bytearray() + udpmsg += bytes([0]) + bytes([esphomeLights[ip]["color"][0]]) + bytes([esphomeLights[ip]["color"][1]]) + bytes([esphomeLights[ip]["color"][2]]) + bytes([esphomeLights[ip]["color"][3]]) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP + sock.sendto(udpmsg, (ip.split(":")[0], 2100)) + except Exception: #Assuming the only exception is a network timeout, please don't scream at me + if syncing: #Reset sync status and kill relay service + logging.info("Entertainment Service was syncing and has timed out, stopping server and clearing state") + Popen(["killall", "entertain-srv"]) + for group in groups.keys(): + if "type" in groups[group] and groups[group]["type"] == "Entertainment": + groups[group]["stream"].update({"active": False, "owner": None}) + syncing = False From e2afe1e44291ca011a7eb17a75a2ad11ae0166d7 Mon Sep 17 00:00:00 2001 From: Silve Date: Thu, 23 Jan 2020 12:00:14 +0300 Subject: [PATCH 12/13] Git's core.autocrlf to false to fix end of the line --- BridgeEmulator/functions/entertainment.py | 234 ++++----- BridgeEmulator/protocols/esphome.py | 592 +++++++++++----------- 2 files changed, 413 insertions(+), 413 deletions(-) diff --git a/BridgeEmulator/functions/entertainment.py b/BridgeEmulator/functions/entertainment.py index 8131b7f36..dbe2f2217 100644 --- a/BridgeEmulator/functions/entertainment.py +++ b/BridgeEmulator/functions/entertainment.py @@ -1,117 +1,117 @@ -import socket, logging -from subprocess import Popen -from functions.colors import convert_rgb_xy, convert_xy -from functions.lightRequest import sendLightRequest - -def entertainmentService(lights, addresses, groups, host_ip): - serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - serverSocket.settimeout(3) #Set a packet timeout that we catch later - serverSocket.bind(('127.0.0.1', 2101)) - frameID = 0 - lightStatus = {} - syncing = False #Flag to check whether or not we had been syncing when a timeout occurs - while True: - try: - data = serverSocket.recvfrom(106)[0] - nativeLights = {} - esphomeLights = {} - if data[:9].decode('utf-8') == "HueStream": - syncing = True #Set sync flag when receiving valid data - if data[14] == 0: #rgb colorspace - i = 16 - while i < len(data): - if data[i] == 0: #Type of device 0x00 = Light - lightId = data[i+1] * 256 + data[i+2] - if lightId != 0: - r = int((data[i+3] * 256 + data[i+4]) / 256) - g = int((data[i+5] * 256 + data[i+6]) / 256) - b = int((data[i+7] * 256 + data[i+8]) / 256) - proto = addresses[str(lightId)]["protocol"] - if lightId not in lightStatus: - lightStatus[lightId] = {"on": False, "bri": 1} - if r == 0 and g == 0 and b == 0: - lights[str(lightId)]["state"]["on"] = False - else: - lights[str(lightId)]["state"].update({"on": True, "bri": int((r + g + b) / 3), "xy": convert_rgb_xy(r, g, b), "colormode": "xy"}) - if proto in ["native", "native_multi", "native_single"]: - if addresses[str(lightId)]["ip"] not in nativeLights: - nativeLights[addresses[str(lightId)]["ip"]] = {} - nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = [r, g, b] - if proto == "esphome": - if addresses[str(lightId)]["ip"] not in esphomeLights: - esphomeLights[addresses[str(lightId)]["ip"]] = {} - bri = int(max(r,g,b)) - esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] - else: - if frameID == 24: # => every seconds, increase in case the destination device is overloaded - gottaSend = False - yee = proto == "yeelight" - brABS = abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) - if r == 0 and g == 0 and b == 0: #Turn off if color is black - if lightStatus[lightId]["on"]: - sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, None, host_ip) - lightStatus[lightId]["on"] = False - else: - if lightStatus[lightId]["on"] == False: #Turn on if color is not black - sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, None, host_ip) - lightStatus[lightId]["on"] = True - elif brABS > 50: # to optimize, send brightness only if difference is bigger than this value - sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 150 / brABS}, lights, addresses, None, host_ip) - lightStatus[lightId]["bri"] = int((r + b + g) / 3) - else: - gottaSend = True - if gottaSend or yee: - sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, [r, g, b], host_ip) - frameID += 1 - if frameID == 25: - frameID = 0 - i = i + 9 - elif data[14] == 1: #cie colorspace - i = 16 - while i < len(data): - if data[i] == 0: #Type of device 0x00 = Light - lightId = data[i+1] * 256 + data[i+2] - if lightId != 0: - x = (data[i+3] * 256 + data[i+4]) / 65535 - y = (data[i+5] * 256 + data[i+6]) / 65535 - bri = int((data[i+7] * 256 + data[i+8]) / 256) - if bri == 0: - lights[str(lightId)]["state"]["on"] = False - else: - lights[str(lightId)]["state"].update({"on": True, "bri": bri, "xy": [x,y], "colormode": "xy"}) - if addresses[str(lightId)]["protocol"] in ["native", "native_multi", "native_single"]: - if addresses[str(lightId)]["ip"] not in nativeLights: - nativeLights[addresses[str(lightId)]["ip"]] = {} - nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = convert_xy(x, y, bri) - if addresses[str(lightId)]["protocol"] == "esphome": - if addresses[str(lightId)]["ip"] not in esphomeLights: - esphomeLights[addresses[str(lightId)]["ip"]] = {} - r, g, b = convert_xy(x, y, bri) - esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] - else: - frameID += 1 - if frameID == 24 : #24 = every seconds, increase in case the destination device is overloaded - sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, None, host_ip) - frameID = 0 - i = i + 9 - if len(nativeLights) is not 0: - for ip in nativeLights.keys(): - udpmsg = bytearray() - for light in nativeLights[ip].keys(): - udpmsg += bytes([light]) + bytes([nativeLights[ip][light][0]]) + bytes([nativeLights[ip][light][1]]) + bytes([nativeLights[ip][light][2]]) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP - sock.sendto(udpmsg, (ip.split(":")[0], 2100)) - if len(esphomeLights) is not 0: - for ip in esphomeLights.keys(): - udpmsg = bytearray() - udpmsg += bytes([0]) + bytes([esphomeLights[ip]["color"][0]]) + bytes([esphomeLights[ip]["color"][1]]) + bytes([esphomeLights[ip]["color"][2]]) + bytes([esphomeLights[ip]["color"][3]]) - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP - sock.sendto(udpmsg, (ip.split(":")[0], 2100)) - except Exception: #Assuming the only exception is a network timeout, please don't scream at me - if syncing: #Reset sync status and kill relay service - logging.info("Entertainment Service was syncing and has timed out, stopping server and clearing state") - Popen(["killall", "entertain-srv"]) - for group in groups.keys(): - if "type" in groups[group] and groups[group]["type"] == "Entertainment": - groups[group]["stream"].update({"active": False, "owner": None}) - syncing = False +import socket, logging +from subprocess import Popen +from functions.colors import convert_rgb_xy, convert_xy +from functions.lightRequest import sendLightRequest + +def entertainmentService(lights, addresses, groups, host_ip): + serverSocket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + serverSocket.settimeout(3) #Set a packet timeout that we catch later + serverSocket.bind(('127.0.0.1', 2101)) + frameID = 0 + lightStatus = {} + syncing = False #Flag to check whether or not we had been syncing when a timeout occurs + while True: + try: + data = serverSocket.recvfrom(106)[0] + nativeLights = {} + esphomeLights = {} + if data[:9].decode('utf-8') == "HueStream": + syncing = True #Set sync flag when receiving valid data + if data[14] == 0: #rgb colorspace + i = 16 + while i < len(data): + if data[i] == 0: #Type of device 0x00 = Light + lightId = data[i+1] * 256 + data[i+2] + if lightId != 0: + r = int((data[i+3] * 256 + data[i+4]) / 256) + g = int((data[i+5] * 256 + data[i+6]) / 256) + b = int((data[i+7] * 256 + data[i+8]) / 256) + proto = addresses[str(lightId)]["protocol"] + if lightId not in lightStatus: + lightStatus[lightId] = {"on": False, "bri": 1} + if r == 0 and g == 0 and b == 0: + lights[str(lightId)]["state"]["on"] = False + else: + lights[str(lightId)]["state"].update({"on": True, "bri": int((r + g + b) / 3), "xy": convert_rgb_xy(r, g, b), "colormode": "xy"}) + if proto in ["native", "native_multi", "native_single"]: + if addresses[str(lightId)]["ip"] not in nativeLights: + nativeLights[addresses[str(lightId)]["ip"]] = {} + nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = [r, g, b] + if proto == "esphome": + if addresses[str(lightId)]["ip"] not in esphomeLights: + esphomeLights[addresses[str(lightId)]["ip"]] = {} + bri = int(max(r,g,b)) + esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] + else: + if frameID == 24: # => every seconds, increase in case the destination device is overloaded + gottaSend = False + yee = proto == "yeelight" + brABS = abs(int((r + b + g) / 3) - lightStatus[lightId]["bri"]) + if r == 0 and g == 0 and b == 0: #Turn off if color is black + if lightStatus[lightId]["on"]: + sendLightRequest(str(lightId), {"on": False, "transitiontime": 3}, lights, addresses, None, host_ip) + lightStatus[lightId]["on"] = False + else: + if lightStatus[lightId]["on"] == False: #Turn on if color is not black + sendLightRequest(str(lightId), {"on": True, "transitiontime": 3}, lights, addresses, None, host_ip) + lightStatus[lightId]["on"] = True + elif brABS > 50: # to optimize, send brightness only if difference is bigger than this value + sendLightRequest(str(lightId), {"bri": int((r + b + g) / 3), "transitiontime": 150 / brABS}, lights, addresses, None, host_ip) + lightStatus[lightId]["bri"] = int((r + b + g) / 3) + else: + gottaSend = True + if gottaSend or yee: + sendLightRequest(str(lightId), {"xy": convert_rgb_xy(r, g, b), "transitiontime": 3}, lights, addresses, [r, g, b], host_ip) + frameID += 1 + if frameID == 25: + frameID = 0 + i = i + 9 + elif data[14] == 1: #cie colorspace + i = 16 + while i < len(data): + if data[i] == 0: #Type of device 0x00 = Light + lightId = data[i+1] * 256 + data[i+2] + if lightId != 0: + x = (data[i+3] * 256 + data[i+4]) / 65535 + y = (data[i+5] * 256 + data[i+6]) / 65535 + bri = int((data[i+7] * 256 + data[i+8]) / 256) + if bri == 0: + lights[str(lightId)]["state"]["on"] = False + else: + lights[str(lightId)]["state"].update({"on": True, "bri": bri, "xy": [x,y], "colormode": "xy"}) + if addresses[str(lightId)]["protocol"] in ["native", "native_multi", "native_single"]: + if addresses[str(lightId)]["ip"] not in nativeLights: + nativeLights[addresses[str(lightId)]["ip"]] = {} + nativeLights[addresses[str(lightId)]["ip"]][addresses[str(lightId)]["light_nr"] - 1] = convert_xy(x, y, bri) + if addresses[str(lightId)]["protocol"] == "esphome": + if addresses[str(lightId)]["ip"] not in esphomeLights: + esphomeLights[addresses[str(lightId)]["ip"]] = {} + r, g, b = convert_xy(x, y, bri) + esphomeLights[addresses[str(lightId)]["ip"]]["color"] = [r, g, b, bri] + else: + frameID += 1 + if frameID == 24 : #24 = every seconds, increase in case the destination device is overloaded + sendLightRequest(str(lightId), {"xy": [x,y]}, lights, addresses, None, host_ip) + frameID = 0 + i = i + 9 + if len(nativeLights) is not 0: + for ip in nativeLights.keys(): + udpmsg = bytearray() + for light in nativeLights[ip].keys(): + udpmsg += bytes([light]) + bytes([nativeLights[ip][light][0]]) + bytes([nativeLights[ip][light][1]]) + bytes([nativeLights[ip][light][2]]) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP + sock.sendto(udpmsg, (ip.split(":")[0], 2100)) + if len(esphomeLights) is not 0: + for ip in esphomeLights.keys(): + udpmsg = bytearray() + udpmsg += bytes([0]) + bytes([esphomeLights[ip]["color"][0]]) + bytes([esphomeLights[ip]["color"][1]]) + bytes([esphomeLights[ip]["color"][2]]) + bytes([esphomeLights[ip]["color"][3]]) + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP + sock.sendto(udpmsg, (ip.split(":")[0], 2100)) + except Exception: #Assuming the only exception is a network timeout, please don't scream at me + if syncing: #Reset sync status and kill relay service + logging.info("Entertainment Service was syncing and has timed out, stopping server and clearing state") + Popen(["killall", "entertain-srv"]) + for group in groups.keys(): + if "type" in groups[group] and groups[group]["type"] == "Entertainment": + groups[group]["stream"].update({"active": False, "owner": None}) + syncing = False diff --git a/BridgeEmulator/protocols/esphome.py b/BridgeEmulator/protocols/esphome.py index aee48d082..cd3b14a88 100644 --- a/BridgeEmulator/protocols/esphome.py +++ b/BridgeEmulator/protocols/esphome.py @@ -1,296 +1,296 @@ -import json -import logging -import random -import requests - -import socket -import sys - -from time import sleep -from subprocess import check_output -from functions import light_types, nextFreeId -from functions.colors import convert_rgb_xy, convert_xy, hsv_to_rgb, rgbBrightness -from functions.network import getIpAddress - -def getRequest(address, request_data, timeout=3): - - head = {"Content-type": "application/json"} - response = requests.get("http://" + address + request_data, timeout=timeout, headers=head) - return response.text - -def postRequest(address, request_data, timeout=3): - head = {"Content-type": "application/json"} - response = requests.post("http://" + address + request_data, timeout=3, headers=head) - return response.text - -def addRequest(request_data, data_type, new_data): - if ("?" in request_data): - request_data = request_data + "&" + str(data_type) + "=" + str(new_data) - else: - request_data = request_data + "?" + str(data_type) + "=" + str(new_data) - return request_data - -def getLightType(light, address, data): - request_data = "" - if address["esphome_model"] == "ESPHome-RGBW": - if "xy" in data: #revised according to hue api docs - request_data = request_data + "/light/color_led" - elif "ct" in data: - request_data = request_data + "/light/white_led" - elif ("hue" in data) or ("sat" in data): - request_data = request_data + "/light/color_led" - else: - if light["state"]["colormode"] == "xy": - request_data = request_data + "/light/color_led" - elif light["state"]["colormode"] == "ct": - request_data = request_data + "/light/white_led" - elif light["state"]["colormode"] == "hs": - request_data = request_data + "/light/color_led" - elif address["esphome_model"] == "ESPHome-CT": - request_data = request_data + "/light/white_led" - elif address["esphome_model"] == "ESPHome-RGB": - request_data = request_data + "/light/color_led" - elif address["esphome_model"] == "ESPHome-Dimmable": - request_data = request_data + "/light/dimmable_led" - elif address["esphome_model"] == "ESPHome-Toggle": - request_data = request_data + "/light/toggle_led" - - return request_data - -def discover(bridge_config, new_lights): - logging.debug("ESPHome: invoked!") - - device_ips = check_output("nmap " + getIpAddress() + "/24 -p80 --open -n | grep report | cut -d ' ' -f5", shell=True).decode('utf-8').rstrip("\n").split("\n") - del device_ips[-1] #delete last empty element in list - for ip in device_ips: - try: - logging.debug ( "ESPHome: probing ip " + ip) - response = requests.get ("http://" + ip + "/text_sensor/light_id", timeout=3) - device = json.loads(response.text)['state'].split(';') #get device data - mac = device[1] - device_name = device[2] - ct_boost = device[3] - rgb_boost = device[4] - if response.status_code == 200 and device[0] == "esphome_diyhue_light": - logging.debug("ESPHome: Found " + device_name + " at ip " + ip) - white_response = requests.get ("http://" + ip + "/light/white_led", timeout=3) - color_response = requests.get ("http://" + ip + "/light/color_led", timeout=3) - dim_response = requests.get ("http://" + ip + "/light/dimmable_led", timeout=3) - toggle_response = requests.get ("http://" + ip + "/light/toggle_led", timeout=3) - - if (white_response.status_code != 200 and color_response.status_code != 200 and dim_response != 200 and toggle_response != 200): - logging.debug("ESPHome: Device has improper configuration! Exiting.") - raise - elif (white_response.status_code == 200 and color_response.status_code == 200): - logging.debug("ESPHome: " + device_name + " is a RGBW ESPHome device") - white_device_data = json.loads(white_response.text) - color_device_data = json.loads(color_response.text) - properties = {"rgb": True, "ct": True, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} - esphome_model = "ESPHome-RGBW" - modelid = "LCT015" - elif (white_response.status_code == 200): - logging.debug("ESPHome: " + device_name + " is a CT ESPHome device") - white_device_data = json.loads(white_response.text) - properties = {"rgb": False, "ct": True, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} - esphome_model = "ESPHome-CT" - modelid = "LWB010" - elif (color_response.status_code == 200): - logging.debug("ESPHome: " + device_name + " is a RGB ESPHome device") - color_device_data = json.loads(color_response.text) - properties = {"rgb": True, "ct": False, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} - esphome_model = "ESPHome-RGB" - modelid = "ESPHome-RGB" - elif (dim_response.status_code == 200): - logging.debug("ESPHome: " + device_name + " is a Dimmable ESPHome device") - dim_device_data = json.loads(dim_response.text) - properties = {"rgb": False, "ct": False, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} - esphome_model = "ESPHome-Dimmable" - modelid = "ESPHome-Dimmable" - elif (toggle_response.status_code == 200): - logging.debug("ESPHome: " + device_name + " is a Toggle ESPHome device") - toggle_device_data = json.loads(toggle_response.text) - properties = {"rgb": False, "ct": False, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} - esphome_model = "ESPHome-Toggle" - modelid = "ESPHome-Toggle" - - device_exist = False - for light in bridge_config["lights_address"].keys(): - if bridge_config["lights_address"][light]["protocol"] == "esphome" and bridge_config["lights_address"][light]["id"].split('.')[0] == properties["id"].split('.')[0]: - device_exist = True - bridge_config["lights_address"][light]["ip"] = properties["ip"] - bridge_config["lights_address"][light]["ct_boost"] = properties["ct_boost"] - bridge_config["lights_address"][light]["rgb_boost"] = properties["rgb_boost"] - logging.debug("ESPHome: light id " + properties["id"] + " already exists, updating device data...") - break - if (not device_exist): - light_name = "ESPHome id " + properties["id"][-8:] if properties["name"] == "" else properties["name"] - logging.debug("ESPHome: Adding ESPHome " + properties["id"]) - new_light_id = nextFreeId(bridge_config, "lights") - bridge_config["lights"][new_light_id] = {} - bridge_config["lights"][new_light_id]["state"] = light_types[modelid]["state"] - bridge_config["lights"][new_light_id]["type"] = light_types[modelid]["type"] - bridge_config["lights"][new_light_id]["name"] = light_name - bridge_config["lights"][new_light_id]["modelid"] = modelid - bridge_config["lights"][new_light_id]["manufacturername"] = light_types[modelid]["manufacturername"] - bridge_config["lights"][new_light_id]["swversion"] = light_types[modelid]["swversion"] - bridge_config["lights"][new_light_id]["config"] = light_types[modelid]["config"] - bridge_config["lights"][new_light_id]["uniqueid"] = mac - if modelid == "LCT015": - bridge_config["lights"][new_light_id]["capabilities"] = light_types[modelid]["capabilities"] - bridge_config["lights"][new_light_id]["streaming"] = light_types[modelid]["streaming"] - #new_lights.update({new_light_id: {"name": light_name}}) - bridge_config["lights_address"][new_light_id] = {} - bridge_config["lights_address"][new_light_id]["ip"] = properties["ip"] - bridge_config["lights_address"][new_light_id]["id"] = properties["id"] - bridge_config["lights_address"][new_light_id]["protocol"] = "esphome" - bridge_config["lights_address"][new_light_id]["rgb_boost"] = rgb_boost - bridge_config["lights_address"][new_light_id]["ct_boost"] = ct_boost - bridge_config["lights_address"][new_light_id]["esphome_model"] = esphome_model - - except Exception as e: - logging.debug("ESPHome: ip " + ip + " is unknown device, " + str(e)) - -def set_light(address, light, data, rgb = None): - logging.debug("ESPHome: invoked! IP=" + address["ip"]) - logging.debug(light["modelid"]) - logging.debug(data) - - ct_boost = int(address["ct_boost"]) - rgb_boost = int(address["rgb_boost"]) - request_data = "" - if ("alert" in data) and (data['alert'] == "select"): #one breath cycle, temporary for now until breath implemented in esphome firmware - request_data = request_data + "/switch/alert/turn_on" - # elif ("alert" in data) and (data['alert'] == "lselect"): #15 second breath cycle - # request_data = request_data + "/switch/alert/turn_on" - # elif ("alert" in data) and (data['alert'] == "none"): - # request_data = request_data + "/switch/alert/turn_off" - else: - request_data = request_data + getLightType(light, address, data) - if "white_led" in request_data: - postRequest(address["ip"], "/light/color_led/turn_off") - else: - postRequest(address["ip"], "/light/white_led/turn_off") - if "on" in data: - if not(data['on']): - request_data = request_data + "/turn_off" - else: - request_data = request_data + "/turn_on" - else: - request_data = request_data + "/turn_on" - if address["esphome_model"] is not "ESPHome-Toggle": - if "bri" in data: - brightness = int(data['bri']) - if address["esphome_model"] == "ESPHome-RGBW": - if light["state"]["colormode"] == "ct": - brightness = ct_boost + brightness - elif light["state"]["colormode"] == "xy": - brightness = rgb_boost + brightness - elif address["esphome_model"] == "ESPHome-CT": - brightness = ct_boost + brightness - elif address["esphome_model"] == "ESPHome-RGB": - brightness = rgb_boost + brightness - elif address["esphome_model"] == "ESPHome-Dimmable": - brightness = ct_boost + brightness - if brightness > 255: # do not send brightness values over 255 - brightness = 255 - request_data = addRequest(request_data, "brightness", brightness) - if address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB", "ESPHome-CT"]: - if ("xy" in data) and (address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB"]): - if rgb: - color = rgbBrightness(rgb, light["state"]["bri"]) - else: - color = convert_xy(data['xy'][0], data['xy'][1], light["state"]["bri"]) - request_data = addRequest(request_data, "r", color[0]) - request_data = addRequest(request_data, "g", color[1]) - request_data = addRequest(request_data, "b", color[2]) - elif "ct" in data and (address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-CT"]): - request_data = addRequest(request_data, "color_temp", data['ct']) - elif (("hue" in data) or ("sat" in data)) and (address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB"]): - if (("hue" in data) and ("sat" in data)): - hue = data['hue'] - sat = data['sat'] - elif "hue" in data: - hue = data['hue'] - sat = light["state"]["sat"] - elif "sat" in data: - hue = light["state"]["hue"] - sat = data['sat'] - if "bri" not in data: - bri = light["state"]["bri"] - else: - bri = data['bri'] - color = hsv_to_rgb(hue, sat, bri) - request_data = addRequest(request_data, "r", color[0]) - request_data = addRequest(request_data, "g", color[1]) - request_data = addRequest(request_data, "b", color[2]) - if "transitiontime" in data: - request_data = addRequest(request_data, "transition", data['transitiontime']/10) - else: #Utilize default interval of 0.4 - request_data = addRequest(request_data, "transition", 0.4) - - postRequest(address["ip"], request_data) - - - - -def get_light_state(address, light): - logging.debug("ESPHome: invoked!") - state = {} - if address["esphome_model"] == "ESPHome-RGBW": - white_response = requests.get ("http://" + address["ip"] + "/light/white_led", timeout=3) - color_response = requests.get ("http://" + address["ip"] + "/light/color_led", timeout=3) - white_device = json.loads(white_response.text) #get white device data - color_device = json.loads(color_response.text) #get color device data - if white_device['state'] == 'OFF' and color_device['state'] == 'OFF': - state['on'] = False - elif white_device['state'] == 'ON': - state['on'] = True - state['ct'] = int(white_device['color_temp']) - state['bri'] = int(white_device['brightness']) - state['colormode'] = "ct" - elif color_device['state'] == 'ON': - state['on'] = True - state['xy'] = convert_rgb_xy(int(color_device['color']['r']), int(color_device['color']['g']), int(color_device['color']['b'])) - state['bri'] = int(color_device['brightness']) - state['colormode'] = "xy" - - elif address["esphome_model"] == "ESPHome-CT": - white_response = requests.get ("http://" + address["ip"] + "/light/white_led", timeout=3) - white_device = json.loads(white_response.text) #get white device data - if white_device['state'] == 'OFF': - state['on'] = False - elif white_device['state'] == 'ON': - state['on'] = True - state['ct'] = int(white_device['color_temp']) - state['bri'] = int(white_device['brightness']) - state['colormode'] = "ct" - - elif address["esphome_model"] == "ESPHome-RGB": - color_response = requests.get ("http://" + address["ip"] + "/light/color_led", timeout=3) - color_device = json.loads(color_response.text) - if color_device['state'] == 'OFF': - state['on'] = False - elif color_device['state'] == 'ON': - state['on'] = True - state['xy'] = convert_rgb_xy(int(color_device['color']['r']), int(color_device['color']['g']), int(color_device['color']['b'])) - state['bri'] = int(color_device['brightness']) - state['colormode'] = "xy" - - elif address["esphome_model"] == "ESPHome-Dimmable": - dimmable_response = requests.get ("http://" + address["ip"] + "/light/dimmable_led", timeout=3) - dimmable_device = json.loads(dimmable_response.text) - if dimmable_device['state'] == 'OFF': - state['on'] = False - elif dimmable_device['state'] == 'ON': - state['on'] = True - state['bri'] = int(dimmable_device['brightness']) - - elif address["esphome_model"] == "ESPHome-Toggle": - toggle_response = requests.get ("http://" + address["ip"] + "/light/toggle_led", timeout=3) - toggle_device = json.loads(toggle_response.text) - if toggle_device['state'] == 'OFF': - state['on'] = False - elif toggle_device['state'] == 'ON': - state['on'] = True - - return state +import json +import logging +import random +import requests + +import socket +import sys + +from time import sleep +from subprocess import check_output +from functions import light_types, nextFreeId +from functions.colors import convert_rgb_xy, convert_xy, hsv_to_rgb, rgbBrightness +from functions.network import getIpAddress + +def getRequest(address, request_data, timeout=3): + + head = {"Content-type": "application/json"} + response = requests.get("http://" + address + request_data, timeout=timeout, headers=head) + return response.text + +def postRequest(address, request_data, timeout=3): + head = {"Content-type": "application/json"} + response = requests.post("http://" + address + request_data, timeout=3, headers=head) + return response.text + +def addRequest(request_data, data_type, new_data): + if ("?" in request_data): + request_data = request_data + "&" + str(data_type) + "=" + str(new_data) + else: + request_data = request_data + "?" + str(data_type) + "=" + str(new_data) + return request_data + +def getLightType(light, address, data): + request_data = "" + if address["esphome_model"] == "ESPHome-RGBW": + if "xy" in data: #revised according to hue api docs + request_data = request_data + "/light/color_led" + elif "ct" in data: + request_data = request_data + "/light/white_led" + elif ("hue" in data) or ("sat" in data): + request_data = request_data + "/light/color_led" + else: + if light["state"]["colormode"] == "xy": + request_data = request_data + "/light/color_led" + elif light["state"]["colormode"] == "ct": + request_data = request_data + "/light/white_led" + elif light["state"]["colormode"] == "hs": + request_data = request_data + "/light/color_led" + elif address["esphome_model"] == "ESPHome-CT": + request_data = request_data + "/light/white_led" + elif address["esphome_model"] == "ESPHome-RGB": + request_data = request_data + "/light/color_led" + elif address["esphome_model"] == "ESPHome-Dimmable": + request_data = request_data + "/light/dimmable_led" + elif address["esphome_model"] == "ESPHome-Toggle": + request_data = request_data + "/light/toggle_led" + + return request_data + +def discover(bridge_config, new_lights): + logging.debug("ESPHome: invoked!") + + device_ips = check_output("nmap " + getIpAddress() + "/24 -p80 --open -n | grep report | cut -d ' ' -f5", shell=True).decode('utf-8').rstrip("\n").split("\n") + del device_ips[-1] #delete last empty element in list + for ip in device_ips: + try: + logging.debug ( "ESPHome: probing ip " + ip) + response = requests.get ("http://" + ip + "/text_sensor/light_id", timeout=3) + device = json.loads(response.text)['state'].split(';') #get device data + mac = device[1] + device_name = device[2] + ct_boost = device[3] + rgb_boost = device[4] + if response.status_code == 200 and device[0] == "esphome_diyhue_light": + logging.debug("ESPHome: Found " + device_name + " at ip " + ip) + white_response = requests.get ("http://" + ip + "/light/white_led", timeout=3) + color_response = requests.get ("http://" + ip + "/light/color_led", timeout=3) + dim_response = requests.get ("http://" + ip + "/light/dimmable_led", timeout=3) + toggle_response = requests.get ("http://" + ip + "/light/toggle_led", timeout=3) + + if (white_response.status_code != 200 and color_response.status_code != 200 and dim_response != 200 and toggle_response != 200): + logging.debug("ESPHome: Device has improper configuration! Exiting.") + raise + elif (white_response.status_code == 200 and color_response.status_code == 200): + logging.debug("ESPHome: " + device_name + " is a RGBW ESPHome device") + white_device_data = json.loads(white_response.text) + color_device_data = json.loads(color_response.text) + properties = {"rgb": True, "ct": True, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} + esphome_model = "ESPHome-RGBW" + modelid = "LCT015" + elif (white_response.status_code == 200): + logging.debug("ESPHome: " + device_name + " is a CT ESPHome device") + white_device_data = json.loads(white_response.text) + properties = {"rgb": False, "ct": True, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} + esphome_model = "ESPHome-CT" + modelid = "LWB010" + elif (color_response.status_code == 200): + logging.debug("ESPHome: " + device_name + " is a RGB ESPHome device") + color_device_data = json.loads(color_response.text) + properties = {"rgb": True, "ct": False, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} + esphome_model = "ESPHome-RGB" + modelid = "ESPHome-RGB" + elif (dim_response.status_code == 200): + logging.debug("ESPHome: " + device_name + " is a Dimmable ESPHome device") + dim_device_data = json.loads(dim_response.text) + properties = {"rgb": False, "ct": False, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} + esphome_model = "ESPHome-Dimmable" + modelid = "ESPHome-Dimmable" + elif (toggle_response.status_code == 200): + logging.debug("ESPHome: " + device_name + " is a Toggle ESPHome device") + toggle_device_data = json.loads(toggle_response.text) + properties = {"rgb": False, "ct": False, "ip": ip, "name": device_name, "id": mac, "mac": mac, "ct_boost": ct_boost, "rgb_boost": rgb_boost} + esphome_model = "ESPHome-Toggle" + modelid = "ESPHome-Toggle" + + device_exist = False + for light in bridge_config["lights_address"].keys(): + if bridge_config["lights_address"][light]["protocol"] == "esphome" and bridge_config["lights_address"][light]["id"].split('.')[0] == properties["id"].split('.')[0]: + device_exist = True + bridge_config["lights_address"][light]["ip"] = properties["ip"] + bridge_config["lights_address"][light]["ct_boost"] = properties["ct_boost"] + bridge_config["lights_address"][light]["rgb_boost"] = properties["rgb_boost"] + logging.debug("ESPHome: light id " + properties["id"] + " already exists, updating device data...") + break + if (not device_exist): + light_name = "ESPHome id " + properties["id"][-8:] if properties["name"] == "" else properties["name"] + logging.debug("ESPHome: Adding ESPHome " + properties["id"]) + new_light_id = nextFreeId(bridge_config, "lights") + bridge_config["lights"][new_light_id] = {} + bridge_config["lights"][new_light_id]["state"] = light_types[modelid]["state"] + bridge_config["lights"][new_light_id]["type"] = light_types[modelid]["type"] + bridge_config["lights"][new_light_id]["name"] = light_name + bridge_config["lights"][new_light_id]["modelid"] = modelid + bridge_config["lights"][new_light_id]["manufacturername"] = light_types[modelid]["manufacturername"] + bridge_config["lights"][new_light_id]["swversion"] = light_types[modelid]["swversion"] + bridge_config["lights"][new_light_id]["config"] = light_types[modelid]["config"] + bridge_config["lights"][new_light_id]["uniqueid"] = mac + if modelid == "LCT015": + bridge_config["lights"][new_light_id]["capabilities"] = light_types[modelid]["capabilities"] + bridge_config["lights"][new_light_id]["streaming"] = light_types[modelid]["streaming"] + #new_lights.update({new_light_id: {"name": light_name}}) + bridge_config["lights_address"][new_light_id] = {} + bridge_config["lights_address"][new_light_id]["ip"] = properties["ip"] + bridge_config["lights_address"][new_light_id]["id"] = properties["id"] + bridge_config["lights_address"][new_light_id]["protocol"] = "esphome" + bridge_config["lights_address"][new_light_id]["rgb_boost"] = rgb_boost + bridge_config["lights_address"][new_light_id]["ct_boost"] = ct_boost + bridge_config["lights_address"][new_light_id]["esphome_model"] = esphome_model + + except Exception as e: + logging.debug("ESPHome: ip " + ip + " is unknown device, " + str(e)) + +def set_light(address, light, data, rgb = None): + logging.debug("ESPHome: invoked! IP=" + address["ip"]) + logging.debug(light["modelid"]) + logging.debug(data) + + ct_boost = int(address["ct_boost"]) + rgb_boost = int(address["rgb_boost"]) + request_data = "" + if ("alert" in data) and (data['alert'] == "select"): #one breath cycle, temporary for now until breath implemented in esphome firmware + request_data = request_data + "/switch/alert/turn_on" + # elif ("alert" in data) and (data['alert'] == "lselect"): #15 second breath cycle + # request_data = request_data + "/switch/alert/turn_on" + # elif ("alert" in data) and (data['alert'] == "none"): + # request_data = request_data + "/switch/alert/turn_off" + else: + request_data = request_data + getLightType(light, address, data) + if "white_led" in request_data: + postRequest(address["ip"], "/light/color_led/turn_off") + else: + postRequest(address["ip"], "/light/white_led/turn_off") + if "on" in data: + if not(data['on']): + request_data = request_data + "/turn_off" + else: + request_data = request_data + "/turn_on" + else: + request_data = request_data + "/turn_on" + if address["esphome_model"] is not "ESPHome-Toggle": + if "bri" in data: + brightness = int(data['bri']) + if address["esphome_model"] == "ESPHome-RGBW": + if light["state"]["colormode"] == "ct": + brightness = ct_boost + brightness + elif light["state"]["colormode"] == "xy": + brightness = rgb_boost + brightness + elif address["esphome_model"] == "ESPHome-CT": + brightness = ct_boost + brightness + elif address["esphome_model"] == "ESPHome-RGB": + brightness = rgb_boost + brightness + elif address["esphome_model"] == "ESPHome-Dimmable": + brightness = ct_boost + brightness + if brightness > 255: # do not send brightness values over 255 + brightness = 255 + request_data = addRequest(request_data, "brightness", brightness) + if address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB", "ESPHome-CT"]: + if ("xy" in data) and (address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB"]): + if rgb: + color = rgbBrightness(rgb, light["state"]["bri"]) + else: + color = convert_xy(data['xy'][0], data['xy'][1], light["state"]["bri"]) + request_data = addRequest(request_data, "r", color[0]) + request_data = addRequest(request_data, "g", color[1]) + request_data = addRequest(request_data, "b", color[2]) + elif "ct" in data and (address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-CT"]): + request_data = addRequest(request_data, "color_temp", data['ct']) + elif (("hue" in data) or ("sat" in data)) and (address["esphome_model"] in ["ESPHome-RGBW", "ESPHome-RGB"]): + if (("hue" in data) and ("sat" in data)): + hue = data['hue'] + sat = data['sat'] + elif "hue" in data: + hue = data['hue'] + sat = light["state"]["sat"] + elif "sat" in data: + hue = light["state"]["hue"] + sat = data['sat'] + if "bri" not in data: + bri = light["state"]["bri"] + else: + bri = data['bri'] + color = hsv_to_rgb(hue, sat, bri) + request_data = addRequest(request_data, "r", color[0]) + request_data = addRequest(request_data, "g", color[1]) + request_data = addRequest(request_data, "b", color[2]) + if "transitiontime" in data: + request_data = addRequest(request_data, "transition", data['transitiontime']/10) + else: #Utilize default interval of 0.4 + request_data = addRequest(request_data, "transition", 0.4) + + postRequest(address["ip"], request_data) + + + + +def get_light_state(address, light): + logging.debug("ESPHome: invoked!") + state = {} + if address["esphome_model"] == "ESPHome-RGBW": + white_response = requests.get ("http://" + address["ip"] + "/light/white_led", timeout=3) + color_response = requests.get ("http://" + address["ip"] + "/light/color_led", timeout=3) + white_device = json.loads(white_response.text) #get white device data + color_device = json.loads(color_response.text) #get color device data + if white_device['state'] == 'OFF' and color_device['state'] == 'OFF': + state['on'] = False + elif white_device['state'] == 'ON': + state['on'] = True + state['ct'] = int(white_device['color_temp']) + state['bri'] = int(white_device['brightness']) + state['colormode'] = "ct" + elif color_device['state'] == 'ON': + state['on'] = True + state['xy'] = convert_rgb_xy(int(color_device['color']['r']), int(color_device['color']['g']), int(color_device['color']['b'])) + state['bri'] = int(color_device['brightness']) + state['colormode'] = "xy" + + elif address["esphome_model"] == "ESPHome-CT": + white_response = requests.get ("http://" + address["ip"] + "/light/white_led", timeout=3) + white_device = json.loads(white_response.text) #get white device data + if white_device['state'] == 'OFF': + state['on'] = False + elif white_device['state'] == 'ON': + state['on'] = True + state['ct'] = int(white_device['color_temp']) + state['bri'] = int(white_device['brightness']) + state['colormode'] = "ct" + + elif address["esphome_model"] == "ESPHome-RGB": + color_response = requests.get ("http://" + address["ip"] + "/light/color_led", timeout=3) + color_device = json.loads(color_response.text) + if color_device['state'] == 'OFF': + state['on'] = False + elif color_device['state'] == 'ON': + state['on'] = True + state['xy'] = convert_rgb_xy(int(color_device['color']['r']), int(color_device['color']['g']), int(color_device['color']['b'])) + state['bri'] = int(color_device['brightness']) + state['colormode'] = "xy" + + elif address["esphome_model"] == "ESPHome-Dimmable": + dimmable_response = requests.get ("http://" + address["ip"] + "/light/dimmable_led", timeout=3) + dimmable_device = json.loads(dimmable_response.text) + if dimmable_device['state'] == 'OFF': + state['on'] = False + elif dimmable_device['state'] == 'ON': + state['on'] = True + state['bri'] = int(dimmable_device['brightness']) + + elif address["esphome_model"] == "ESPHome-Toggle": + toggle_response = requests.get ("http://" + address["ip"] + "/light/toggle_led", timeout=3) + toggle_device = json.loads(toggle_response.text) + if toggle_device['state'] == 'OFF': + state['on'] = False + elif toggle_device['state'] == 'ON': + state['on'] = True + + return state From 6993f55bb622640c39c24bedb2ac5f438d23c5e3 Mon Sep 17 00:00:00 2001 From: SilveIT Date: Mon, 27 Jan 2020 03:54:51 +0300 Subject: [PATCH 13/13] White color logic + small disconnect optimization (#341) * Small changes to disconnect logic * Controversial changes relative to white * Using CT instead of RGB where color is nearly white; * Revert "Controversial changes relative to white" This reverts commit 1768f7f8e53bbf124d25b55c465061e5418dca97. --- BridgeEmulator/protocols/yeelight.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/BridgeEmulator/protocols/yeelight.py b/BridgeEmulator/protocols/yeelight.py index 9a0051fd4..486cc4281 100644 --- a/BridgeEmulator/protocols/yeelight.py +++ b/BridgeEmulator/protocols/yeelight.py @@ -81,6 +81,13 @@ def command(ip, light, api_method, param): c.disconnect() def set_light(address, light, data, rgb = None): + ip = address["ip"] + if ip in Connections: + c = Connections[ip] + else: + c = YeelightConnection(ip) + Connections[ip] = c + method = 'TCP' payload = {} transitiontime = 400 @@ -117,7 +124,14 @@ def set_light(address, light, data, rgb = None): # see page 9 http://www.yeelight.com/download/Yeelight_Inter-Operation_Spec.pdf # check if hue wants to change brightness for key, value in payload.items(): - command(address["ip"], light, key, value) + try: + c.command(key, value) + except Exception as e: + if not c._music and c._connected: + c.disconnect() + raise e + if not c._music and c._connected: + c.disconnect() def get_light_state(address, light): #logging.info("name is: " + light["name"])