From 1ae3af4c59e54042997202d0ea32f7f611af568e Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 11 Jan 2017 16:24:43 +0300 Subject: [PATCH] Init --- .gitignore | 93 ++++++++++++++++++++++++++++++ Dockerfile | 12 ++++ README.md | 25 ++++++++ requirements.txt | 2 + src/main.py | 132 +++++++++++++++++++++++++++++++++++++++++++ src/plugstate.py | 78 +++++++++++++++++++++++++ src/requirements.txt | 2 + 7 files changed, 344 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 requirements.txt create mode 100644 src/main.py create mode 100644 src/plugstate.py create mode 100644 src/requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..827de0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,93 @@ +#docker-compose file, because it contains passwords +docker-compose.yaml +docker-compose.yml + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# IPython Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2ed75ff --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM jfloff/alpine-python:3.4 + +ENV LIBRARY_PATH=/lib:/usr/lib + +ADD src/requirements.txt / +RUN pip install --upgrade pip && pip install -r /requirements.txt + +WORKDIR /app +COPY src /app + +EXPOSE 5000 +CMD ["python", "-u", "/app/main.py"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fdb019 --- /dev/null +++ b/README.md @@ -0,0 +1,25 @@ +# Yeelight to MQTT bridge + +Works with Yeelight WiFi bulbs (color and monochrome). +You need to edit you bulb IPs in main.py (not yet fixed). + +Sample docker-compose.yml file: +``` +yeelight: + build: . + container_name: yeelight + environment: + - MQTT_SERVER=192.168.1.93 + - MQTT_USER=mqtt_user + - MQTT_PASS=passw0rd + restart: always +``` + +``` +docker-compose build && docker-compose up -d +``` + +## Known bugs: +- Lamp's alive status updated only at startup. +- IP-Name list in main.py +- Bad code style =( \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0ce3fb1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +broadlink +paho-mqtt diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..ff44683 --- /dev/null +++ b/src/main.py @@ -0,0 +1,132 @@ +import broadlink +import paho.mqtt.client as mqtt +import os +import time +import threading +from plugstate import PlugState + +MQTT_SERVER = os.getenv('MQTT_SERVER', "192.168.1.93") +MQTT_PORT = os.getenv('MQTT_PORT', 1883) +MQTT_USER = os.getenv('MQTT_USER', "hass") +MQTT_PASS = os.getenv('MQTT_PASS', "mqttpass") +QUERY_TIME = os.getenv('QUERY_TIME', 5) +PREFIX = os.getenv('PREFIX', 'home') + +ips = dict({ + '192.168.1.103':{'name':'kitchen-right', 'mac':bytearray(b'vr\xcb\rC\xb4'), 'type':'plug'}, + '192.168.1.183':{'name':'balcony-heater', 'mac':bytearray(b'\xc1j\xcb\rC\xb4'), 'type':'plug'}, + '192.168.1.188':{'name':'kitchen-kettle', 'mac':bytearray(b'9V\xcb\rC\xb4'), 'type':'plug'}, + '192.168.1.49':{'name':'rm2', 'mac':bytearray(b'U"\xcc\rC\xb4'), 'type':'remote'} +}) + +plugs=[] +processNow = False + +PATH_FMT = PREFIX + "/{model}/{sid}/{prop}" # short_id or sid ? + +def prepare_mqtt(): + print("Connecting to MQTT server", MQTT_SERVER, ":", MQTT_PORT, "with username", MQTT_USER,":",MQTT_PASS) + client = mqtt.Client() + if (MQTT_USER != "" and MQTT_PASS != ""): + client.username_pw_set(MQTT_USER, MQTT_PASS) + client.connect(MQTT_SERVER, MQTT_PORT, 60) + + return client + +def push_data(client, model, sid, data): + for key, value in data.items(): + path = PATH_FMT.format(model=model, + sid=sid, + prop=key) + client.publish(path, payload=value, qos=0, retain=True) + +def init_plugs(): + plugs=[] + for ip in ips: + try: + props = ips[ip] + name = props['name'] + mac = props['mac'] + typeName = props['type'] + typeId = 0x2720 #SP2 by default + if (typeName == "remote"): + typeId = 0x2712 + broadObj = broadlink.gendevice(typeId, (ip, 80) , mac) + broadObj.auth() + plug = PlugState(ip, typeName, name, broadObj) + plug.update_properties() + plugs.append(plug) + except Exception as e: + print('Connection to ', str(ip) , ' error:', str(e)) + return plugs + +def refresh_plug_states(data_callback): + global plugs + for plug in plugs: + try: + hashold = plug.hash() + plug.update_properties(force=True) + hashnew = plug.hash() + # print(str(plug.name), hashold, hashnew) + + if (hashold != hashnew): + print("!!!! ", plug.name, ":", hashold, "->", hashnew) + data = {'status':plug.status} + if (plug.type == "remote"): + data = {'code':plug.learned_code} + if data_callback is not None: + data_callback(plug.type, plug.name, data) + except Exception as e: + print('Connection to ', str(plug.name) , ' error:', str(e)) + +def on_mqtt_message(client, userdata, msg): + global processNow + print(msg.topic+" "+str(msg.payload)) + parts = msg.topic.split("/") + if (len(parts) != 5): + return + type = parts[1] + if (type != "plug" and type != "remote"): + return + name = parts[2] #name part + param = parts[3] #param part + value = (msg.payload).decode('utf-8') + + for plug in plugs: + if (plug.name != name): + continue + plug.process_command(param, value) + processNow=True + +def on_connect(client, userdata, rc): + client.subscribe(PREFIX + "/plug/+/+/set") + client.subscribe(PREFIX + "/remote/+/+/learn") + client.subscribe(PREFIX + "/remote/+/+/set") + +def refresh_loop(client): + global processNow + cb = lambda m, s, d: push_data(client, m, s, d) + while True: + refresh_plug_states(cb) + for x in range(1,10): + if (processNow): + processNow=False + break + time.sleep(QUERY_TIME/10) + +if __name__ == "__main__": + client = prepare_mqtt() + plugs = init_plugs() + print("Founded devices:") + for plug in plugs: + print("Device", plug.name, "on ip", plug.ip, "with type", plug.type) + + client.on_message = on_mqtt_message + client.on_connect = on_connect + + #start thread for lamp refresh loop + t1 = threading.Thread(target=refresh_loop, args=[client]) + t1.start() + + # and process mqtt messages in this thread + client.loop_forever() \ No newline at end of file diff --git a/src/plugstate.py b/src/plugstate.py new file mode 100644 index 0000000..7cf87ad --- /dev/null +++ b/src/plugstate.py @@ -0,0 +1,78 @@ +import broadlink +import os +import time +import threading +import binascii + +class PlugState: + status = "off" + ip = "" + name = "" + type = "" + learned_code = "" + + learn_temp = "" + broadlink = None # yeelight object + + + def __init__(self, ip, typeName, name, broadlinkObj): + self.broadlink = broadlinkObj + self.name = name + self.type = typeName + self.ip = ip + + def update_properties(self, force = False): + #need to copy nearned code + if (self.type == "remote" and force): + self.learned_code = self.learn_temp + self.learn_temp = "" + + if (self.type == "plug" and force): + state = self.broadlink.check_power() + if (state): + self.status = "on" + else: + self.status = "off" + + def hash(self): + return str(self.status) + ":" + str(self.learned_code) + + def is_int(self, x): + try: + tmp = int(x) + return True + except Exception as e: + return False + + def process_command(self, param, value): + try: + if (param == 'status'): + if (value == "on"): + print("Turning on plug", self.name) + self.broadlink.set_power(True) + if (value == "off"): + print("Turning off plug", self.name) + self.broadlink.set_power(False) + if (param == 'learn' and self.type == 'remote'): + self.broadlink.enter_learning() + time.sleep(5) + ir_packet = self.broadlink.check_data() + #sample data + # ir_packet = bytearray([0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x08 ,0x00 ,0x00 ,0x00 ,0xe0 ,0x07 ,0x25 ,0x32 ,0x07 ,0x01 ,0x03 ,0x0a ,0x00 ,0x00 ,0x00 ,0x00 ,0xc0 ,0xa8 ,0x00 ,0x81 ,0x53 ,0xf1 ,0x00 ,0x00 ,0x3d ,0xc3 ,0x00 ,0x00 ,0x00 ,0x00 ,0x06 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00]) + if (ir_packet == None): + print("No ir packets recieved") + return + self.learn_temp = binascii.hexlify(ir_packet) + print("Learned code:", self.learn_temp) + if (param == 'code' and self.type == 'remote'): + if (value == None or value == ""): + print("No ir packets in value") + return + print("Sending IR packet:", value) + #sample data + # ir_packet = bytearray([0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x08 ,0x00 ,0x00 ,0x00 ,0xe0 ,0x07 ,0x25 ,0x32 ,0x07 ,0x01 ,0x03 ,0x0a ,0x00 ,0x00 ,0x00 ,0x00 ,0xc0 ,0xa8 ,0x00 ,0x81 ,0x53 ,0xf1 ,0x00 ,0x00 ,0x3d ,0xc3 ,0x00 ,0x00 ,0x00 ,0x00 ,0x06 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00 ,0x00]) + ir_packet = bytes.fromhex(value) + self.broadlink.send_data(ir_packet) + + except Exception as e: + print ('Error while set value of plug', self.name , ' error:', e) \ No newline at end of file diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..0ce3fb1 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,2 @@ +broadlink +paho-mqtt