diff --git a/.gitignore b/.gitignore index d07ed69..2e5dea5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,11 @@ instance .vscode gunicorn_config.py function.zip -lambda_function/package \ No newline at end of file +lambda_function/package +.installed.cfg +__pycache__/ +bin/ +buildout.cfg +eggs/ +parts/ +var/ diff --git a/Makefile b/Makefile index 7c2ca2e..cbf923c 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,11 @@ .PHONY: install install: pipenv install + pipenv run buildout -N + +.PHONY: buildout +buildout: + pipenv run buildout -N .PHONY: dev dev: diff --git a/Pipfile b/Pipfile index 7fa7772..70eb29e 100644 --- a/Pipfile +++ b/Pipfile @@ -6,12 +6,7 @@ verify_ssl = true [dev-packages] [packages] -flask = "*" -flask-mqtt = "*" -flask-restful = "*" -gunicorn = "*" -flask-socketio = "*" -gevent = "*" +zc-buildout = "*" [requires] python_version = "3.7" diff --git a/Pipfile.lock b/Pipfile.lock index 162fdf5..9e2d699 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e0e5d17471bc087dbf8704243fc004fe0e3e9cc1da4a231914293a0171acc3c8" + "sha256": "4e3b71cce1889889b0896a7803c2bc7310c6c086751ef4c8e98e45093fe00d23" }, "pipfile-spec": 6, "requires": { @@ -16,208 +16,12 @@ ] }, "default": { - "aniso8601": { + "zc-buildout": { "hashes": [ - "sha256:529dcb1f5f26ee0df6c0a1ee84b7b27197c3c50fc3a6321d66c544689237d072", - "sha256:c033f63d028b9a58e3ab0c2c7d0532ab4bfa7452bfc788fbfe3ddabd327b181a" - ], - "version": "==8.0.0" - }, - "click": { - "hashes": [ - "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", - "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" - ], - "version": "==7.0" - }, - "flask": { - "hashes": [ - "sha256:13f9f196f330c7c2c5d7a5cf91af894110ca0215ac051b5844701f2bfd934d52", - "sha256:45eb5a6fd193d6cf7e0cf5d8a5b31f83d5faae0293695626f539a823e93b13f6" - ], - "index": "pypi", - "version": "==1.1.1" - }, - "flask-mqtt": { - "hashes": [ - "sha256:a12e982144fe5a5b0bd3501907c84c80c1cda9c5718eb473ecbd21431fff287e" - ], - "index": "pypi", - "version": "==1.0.5" - }, - "flask-restful": { - "hashes": [ - "sha256:ecd620c5cc29f663627f99e04f17d1f16d095c83dc1d618426e2ad68b03092f8", - "sha256:f8240ec12349afe8df1db168ea7c336c4e5b0271a36982bff7394f93275f2ca9" - ], - "index": "pypi", - "version": "==0.3.7" - }, - "flask-socketio": { - "hashes": [ - "sha256:2172dff1e42415ba480cee02c30c2fc833671ff326f1598ee3d69aa02cf768ec", - "sha256:7ff5b2f5edde23e875a8b0abf868584e5706e11741557449bc5147df2cd78268" - ], - "index": "pypi", - "version": "==4.2.1" - }, - "gevent": { - "hashes": [ - "sha256:0774babec518a24d9a7231d4e689931f31b332c4517a771e532002614e270a64", - "sha256:0e1e5b73a445fe82d40907322e1e0eec6a6745ca3cea19291c6f9f50117bb7ea", - "sha256:0ff2b70e8e338cf13bedf146b8c29d475e2a544b5d1fe14045aee827c073842c", - "sha256:107f4232db2172f7e8429ed7779c10f2ed16616d75ffbe77e0e0c3fcdeb51a51", - "sha256:14b4d06d19d39a440e72253f77067d27209c67e7611e352f79fe69e0f618f76e", - "sha256:1b7d3a285978b27b469c0ff5fb5a72bcd69f4306dbbf22d7997d83209a8ba917", - "sha256:1eb7fa3b9bd9174dfe9c3b59b7a09b768ecd496debfc4976a9530a3e15c990d1", - "sha256:2711e69788ddb34c059a30186e05c55a6b611cb9e34ac343e69cf3264d42fe1c", - "sha256:28a0c5417b464562ab9842dd1fb0cc1524e60494641d973206ec24d6ec5f6909", - "sha256:3249011d13d0c63bea72d91cec23a9cf18c25f91d1f115121e5c9113d753fa12", - "sha256:44089ed06a962a3a70e96353c981d628b2d4a2f2a75ea5d90f916a62d22af2e8", - "sha256:4bfa291e3c931ff3c99a349d8857605dca029de61d74c6bb82bd46373959c942", - "sha256:50024a1ee2cf04645535c5ebaeaa0a60c5ef32e262da981f4be0546b26791950", - "sha256:53b72385857e04e7faca13c613c07cab411480822ac658d97fd8a4ddbaf715c8", - "sha256:74b7528f901f39c39cdbb50cdf08f1a2351725d9aebaef212a29abfbb06895ee", - "sha256:7d0809e2991c9784eceeadef01c27ee6a33ca09ebba6154317a257353e3af922", - "sha256:896b2b80931d6b13b5d9feba3d4eebc67d5e6ec54f0cf3339d08487d55d93b0e", - "sha256:8d9ec51cc06580f8c21b41fd3f2b3465197ba5b23c00eb7d422b7ae0380510b0", - "sha256:9f7a1e96fec45f70ad364e46de32ccacab4d80de238bd3c2edd036867ccd48ad", - "sha256:ab4dc33ef0e26dc627559786a4fba0c2227f125db85d970abbf85b77506b3f51", - "sha256:d1e6d1f156e999edab069d79d890859806b555ce4e4da5b6418616322f0a3df1", - "sha256:d752bcf1b98174780e2317ada12013d612f05116456133a6acf3e17d43b71f05", - "sha256:e5bcc4270671936349249d26140c267397b7b4b1381f5ec8b13c53c5b53ab6e1" + "sha256:968e318d1a8c46acab0e5feddb16358fccc4f7f3c2a3b425a02f3471e2209570" ], "index": "pypi", - "version": "==1.4.0" - }, - "greenlet": { - "hashes": [ - "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", - "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", - "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", - "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", - "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", - "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", - "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", - "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", - "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", - "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", - "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", - "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", - "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", - "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", - "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", - "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", - "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", - "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", - "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" - ], - "markers": "platform_python_implementation == 'CPython'", - "version": "==0.4.15" - }, - "gunicorn": { - "hashes": [ - "sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626", - "sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c" - ], - "index": "pypi", - "version": "==20.0.4" - }, - "itsdangerous": { - "hashes": [ - "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", - "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" - ], - "version": "==1.1.0" - }, - "jinja2": { - "hashes": [ - "sha256:74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", - "sha256:9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de" - ], - "version": "==2.10.3" - }, - "markupsafe": { - "hashes": [ - "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", - "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", - "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", - "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", - "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", - "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", - "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", - "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", - "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", - "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", - "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", - "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", - "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", - "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", - "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", - "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", - "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", - "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", - "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", - "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", - "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", - "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", - "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", - "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", - "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", - "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", - "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", - "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" - ], - "version": "==1.1.1" - }, - "paho-mqtt": { - "hashes": [ - "sha256:e3d286198baaea195c8b3bc221941d25a3ab0e1507fc1779bdb7473806394be4" - ], - "version": "==1.5.0" - }, - "python-engineio": { - "hashes": [ - "sha256:4a13fb87c819b855c55a731fdf82559adb8311c04cfdfebd6b9ecd1c2afbb575", - "sha256:9c9a6035b4b5e5a225f426f846afa14cf627f7571d1ae02167cb703fefd134b7" - ], - "version": "==3.10.0" - }, - "python-socketio": { - "hashes": [ - "sha256:48cba5b827ac665dbf923a4f5ec590812aed5299a831fc43576a9af346272534", - "sha256:af6c23c35497960f82106e36688123ecb52ad5a77d0ca27954ff3811c4d9d562" - ], - "version": "==4.4.0" - }, - "pytz": { - "hashes": [ - "sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", - "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be" - ], - "version": "==2019.3" - }, - "six": { - "hashes": [ - "sha256:1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", - "sha256:30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66" - ], - "version": "==1.13.0" - }, - "typing": { - "hashes": [ - "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", - "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", - "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714" - ], - "version": "==3.7.4.1" - }, - "werkzeug": { - "hashes": [ - "sha256:7280924747b5733b246fe23972186c6b348f9ae29724135a6dfc1e53cea433e7", - "sha256:e5f4a1f98b52b18a93da705a7458e55afb26f32bff83ff5d19189f92462d65c4" - ], - "version": "==0.16.0" + "version": "==2.13.1" } }, "develop": {} diff --git a/README.md b/README.md index f7c1b5f..cf6fbc4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,55 @@ -# Roller Shutter +# Roller Shutter Manager -TODO +This is a Flask app that allows to manage a set of Shelly 2.5 switches mounted to control roller shutters.àà# + +## Prerequisites + +- mqtt broker running (i use mosquitto) + +## App configuration + +We use [Instance folders](https://flask.palletsprojects.com/en/1.1.x/config/#instance-folders) configuration, +so you need to create a configuration file in `instance/config.py` with informations like these:: + + MQTT_BROKER_URL = "127.0.0.1" + MQTT_BROKER_PORT = 1883 + FLASK_APP = "blinds_manager" + + BLINDS = [ + {"id": "blind1", "name": "Kitchen"}, + ] + +Where `MQTT_*` are config informations for MQTT broker connection. + +BLINDS is a list of dictionaries where `id` is the blind id set into MQTT settings into Shelly device. + +## Installation + +This app runs with a buildout configuration, to handle dependencies and helper scripts. + +First of all you need to create a virtualenv and install zc.buildout. + +If you are using `pipenv`, just run:: + + > pipenv + +It will create a new virtual environment with Python 3.7 and zc.buildout. + +After that, you have to run the buildout to install all the needed dependencies. +There are two main config files (development.cfg and production.cfg). Symlink buildout.cfg with one of these +depending on the environment where you are working on.:: + + > ln -s development.cfg buildout.cfg + +And finally, run buildout:: + + pipenv run buildout -N + +Or with Makefile:: + + > make buildout + +This will install all Flask dependencies and supervisor. ## Production mode diff --git a/app.py b/app.py index b2812f6..96574f9 100644 --- a/app.py +++ b/app.py @@ -7,7 +7,9 @@ import os import re -app = Flask(__name__, instance_relative_config=True, static_folder="static/build") +app = Flask( + __name__, instance_relative_config=True, static_folder="static/build" +) app.config.from_pyfile("config.py") api = Api(app) @@ -23,7 +25,9 @@ def handle_connect(client, userdata, flags, rc): # mqtt.subscribe("shellies/command") for blind in app.config.get("BLINDS", []): mqtt.subscribe("shellies/{id}/online".format(id=blind.get("id", ""))) - mqtt.subscribe("shellies/{id}/roller/0/pos".format(id=blind.get("id", ""))) + mqtt.subscribe( + "shellies/{id}/roller/0/pos".format(id=blind.get("id", "")) + ) mqtt.subscribe("shellies/{id}/roller/0".format(id=blind.get("id", ""))) @@ -77,7 +81,12 @@ def publish(self, id, action): def get(self, id, action): if action not in ["close", "open", "stop"]: - return {"message": 'Valid actions are "close", "open", "stop" or "rc"'}, 400 + return ( + { + "message": 'Valid actions are "close", "open", "stop" or "rc"' + }, + 400, + ) if id == "all": for blind in app.config.get("BLINDS", []): self.publish(id=blind.get("id", ""), action=action) @@ -100,6 +109,4 @@ def get(self, id, action): api.add_resource(Action, "/roller//") if __name__ == "__main__": - socketio.run( - app, debug=False, host="0.0.0.0", threaded=True, - ) + socketio.run(app, debug=True, host="0.0.0.0", threaded=False) diff --git a/base.cfg b/base.cfg new file mode 100644 index 0000000..f2d7b7d --- /dev/null +++ b/base.cfg @@ -0,0 +1,27 @@ +[buildout] + +extends = + flask.cfg + versions.cfg + +eggs-directory = eggs +versions = versions +show-picked-versions = true + +parts = + flask + supervisor + remove_typings + +[supervisor] +recipe = collective.recipe.supervisor +http-socket = unix +user = admin +password = admin +file = ${buildout:directory}/var/supervisor.sock +programs = + +[remove_typings] +recipe = collective.recipe.cmd +cmds = + "rm -rf eggs/typing*" diff --git a/development.cfg b/development.cfg new file mode 100644 index 0000000..09b4f1c --- /dev/null +++ b/development.cfg @@ -0,0 +1,17 @@ +[buildout] + +extends = + base.cfg + +parts += + flask-instance + + +[flask-instance] +recipe = zc.zdaemonrecipe +program = + ${buildout:bin-directory}/flask run + +[supervisor] +programs = + 100 mosquitto /usr/local/Cellar/mosquitto/1.6.8/sbin/mosquitto [-c /usr/local/etc/mosquitto/mosquitto.conf] true diff --git a/flask.cfg b/flask.cfg new file mode 100644 index 0000000..e7fe1b5 --- /dev/null +++ b/flask.cfg @@ -0,0 +1,13 @@ +[buildout] +parts = + flask + +[flask] +recipe = zc.recipe.egg +eggs = + flask + flask-mqtt + flask-restful + gunicorn + flask-socketio + gevent diff --git a/production.cfg b/production.cfg new file mode 100644 index 0000000..2e1b260 --- /dev/null +++ b/production.cfg @@ -0,0 +1,4 @@ +[buildout] +programs = + 100 mosquitto /usr/local/Cellar/mosquitto/1.6.8/sbin/mosquitto [-c /usr/local/etc/mosquitto/mosquitto.conf] true + 200 gunicorn ${buildout:directory}/bin/gunicorn [-c gunicorn_config.py wsgi:app] true diff --git a/versions.cfg b/versions.cfg new file mode 100644 index 0000000..7597d02 --- /dev/null +++ b/versions.cfg @@ -0,0 +1,33 @@ +[versions] +Click = 7.0 +Flask = 1.1.1 +Flask-MQTT = 1.0.5 +Flask-RESTful = 0.3.7 +Flask-SocketIO = 4.2.1 +Jinja2 = 2.10.1 +MarkupSafe = 1.1.1 +Werkzeug = 0.16.0 +aniso8601 = 8.0.0 +collective.recipe.supervisor = 1.0.0 +gevent = 1.4.0 +greenlet = 0.4.15 +gunicorn = 20.0.4 +itsdangerous = 1.1.0 +meld3 = 1.0.2 +python-engineio = 3.11.1 +python-socketio = 4.4.0 +six = 1.12.0 +supervisor = 4.0.3 +zc.recipe.egg = 2.0.7 + +# Required by: +# Flask-MQTT==1.0.5 +paho-mqtt = 1.5.0 + +# Required by: +# Flask-RESTful==0.3.7 +pytz = 2019.1 + +# Required by: +# Flask-MQTT==1.0.5 +typing = 3.7.4.1