diff --git a/.gitignore b/.gitignore index 2e5dea5..032f713 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ buildout.cfg eggs/ parts/ var/ +develop-eggs/ +src/ +.mr.developer.cfg \ No newline at end of file diff --git a/Makefile b/Makefile index 2711da2..603ab96 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,7 @@ buildout: .PHONY: dev dev: - pipenv run python app.py - -dev-ssl: - pipenv run python app.py --cert /Users/cekk/letsencrypt/config/live/cekk.changeip.co/fullchain.pem --key /Users/cekk/letsencrypt/config/live/cekk.changeip.co/privkey.pem + FLASK_APP=dev.py bin/flask run -p 8000 prod: bin/gunicorn -c gunicorn_config.py wsgi:app diff --git a/app.py b/app.py index 916f8d6..948f09f 100644 --- a/app.py +++ b/app.py @@ -1,49 +1,35 @@ # -*- coding: utf-8 -*- from flask import Blueprint from flask import Flask -from flask import Flask +from flask import current_app from flask import render_template from flask import request -from flask import send_from_directory -from flask_mqtt import Mqtt -from flask_restful import Resource, Api, reqparse, abort -from flask_socketio import SocketIO +from flask_restful import abort +from flask_restful import Api +from flask_restful import reqparse +from flask_restful import Resource +from functools import wraps +from routes.api import api_bp +from routes.frontend import frontend from utils import createSocketMessage +from utils import mqtt +from utils import socketio + from flask_jwt_extended import ( JWTManager, jwt_required, create_access_token, get_jwt_identity, ) -from functools import wraps + import logging import os import re -app = Flask(__name__, instance_relative_config=True, static_folder="static/build") -app.config.from_pyfile("config.py") - -api_bp = Blueprint("api", __name__) -api = Api(api_bp) -app.register_blueprint(api_bp) - -socketio = SocketIO(app) -mqtt = Mqtt(app) -jwt = JWTManager(app) - parser = reqparse.RequestParser() parser.add_argument("command", type=str, help="") -@mqtt.on_connect() -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".format(id=blind.get("id", ""))) - - @mqtt.on_message() def handle_mqtt_message(client, userdata, message): if message.payload == "announce": @@ -60,84 +46,24 @@ def connect_handler(): mqtt.publish("shellies/command", "announce") -# @app.route("/", defaults={"path": ""}) -# @app.route("/") -# def serve(path): -# if path != "" and os.path.exists(app.static_folder + "/" + path): -# return send_from_directory(app.static_folder, path) -# else: -# app.logger.info("AAAA") -# app.logger.debug("BBB") -# app.logger.warning("CCC") -# app.logger.error("DDD") -# return send_from_directory(app.static_folder, "index.html") - - -class ProtectedResource(Resource): - method_decorators = [jwt_required] +def create_app(debug=False, local=False): + """Create an application.""" + app = Flask(__name__, instance_relative_config=True, static_folder="static/build") + app.config.from_pyfile("config.py") + api = Api(api_bp) + app.register_blueprint(api_bp) - def check_skill_id(*args, **kwargs): - if not app.config["SKILL_ID"]: - abort(401) - skill_id = get_jwt_identity() - if skill_id != app.config["SKILL_ID"]: - abort(401) + if local: + app.config["LOCAL"] = True + app.register_blueprint(frontend) + jwt = JWTManager(app) + socketio.init_app(app) + mqtt.init_app(app) -class Blinds(ProtectedResource): - def get(self): - self.check_skill_id() - return app.config.get("BLINDS", []) - - -class Announce(Resource): - def get(self): - # force all shellies to announce their status - mqtt.publish("shellies/command", "announce") - return "", 204 - - -class Update(Resource): - def get(self): - # force all shellies to announce their status - mqtt.publish("shellies/command", "update") - return "", 204 - - -class Action(ProtectedResource): - def publish(self, id, action): - mqtt.publish("shellies/{id}/roller/0/command".format(id=id), action) - - def get(self, id, action): - self.check_skill_id() - if action not in ["close", "open", "stop"]: - 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) - else: - self.publish(id=id, action=action) - return "", 204 - - -# class Command(Resource): -# def post(self): -# args = parser.parse_args() -# mqtt.publish("shellies/shellyswitch25-68E5F8/roller/0/command", "close") -# return {todo_id: todos[todo_id]} - - -api.add_resource(Blinds, "/blinds") -api.add_resource(Announce, "/announce") -api.add_resource(Update, "/update") -# api.add_resource(Command, "/command") -api.add_resource(Action, "/roller//") + 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".format(id=blind.get("id", ""))) -if __name__ == "__main__": - gunicorn_logger = logging.getLogger("gunicorn.error") - app.logger.handlers = gunicorn_logger.handlers - app.logger.setLevel(gunicorn_logger.level) - socketio.run(app, debug=True, host="0.0.0.0", port=4000) + return app diff --git a/dev.py b/dev.py new file mode 100644 index 0000000..c8fa8b8 --- /dev/null +++ b/dev.py @@ -0,0 +1,4 @@ +from app import create_app +from app import socketio + +app = create_app(debug=True, local=True) diff --git a/flask.cfg b/flask.cfg index 85bcde9..2cd3558 100644 --- a/flask.cfg +++ b/flask.cfg @@ -6,9 +6,10 @@ parts = recipe = zc.recipe.egg eggs = flask + flask_jwt_extended flask-mqtt flask-restful - gunicorn flask-socketio gevent - flask_jwt_extended + gunicorn + requests diff --git a/static/.nvmrc b/static/.nvmrc new file mode 100644 index 0000000..2bf5ad0 --- /dev/null +++ b/static/.nvmrc @@ -0,0 +1 @@ +stable diff --git a/static/package.json b/static/package.json index 1410dcd..cab251b 100644 --- a/static/package.json +++ b/static/package.json @@ -8,6 +8,7 @@ "@emotion/styled": "^10.0.23", "axios": "^0.19.0", "emotion-theming": "^10.0.19", + "lodash.isequal": "^4.5.0", "react": "^16.12.0", "react-dom": "^16.12.0", "react-icons": "^3.8.0", @@ -35,5 +36,5 @@ "last 1 safari version" ] }, - "proxy": "http://localhost:5000" + "proxy": "http://localhost:8000" } diff --git a/static/src/App.js b/static/src/App.js index 5deb238..94af3d1 100644 --- a/static/src/App.js +++ b/static/src/App.js @@ -5,6 +5,7 @@ import { Heading, Box } from "@chakra-ui/core"; import BlindsList from "./BlindsList"; import io from "socket.io-client"; import axios from "axios"; +import isEqual from "lodash.isequal"; class App extends Component { constructor() { @@ -18,6 +19,11 @@ class App extends Component { updateBlindInfos = data => { const { blinds } = this.state; + const blind = blinds.filter(device => device.id === data.id)[0]; + if (isEqual(blind, { ...blind, ...data })) { + // no changes + return; + } const updatedValues = blinds.map(blind => { if (blind.id !== data.id) { return blind; diff --git a/static/src/BlindItem.js b/static/src/BlindItem.js index 5697769..c74e41f 100644 --- a/static/src/BlindItem.js +++ b/static/src/BlindItem.js @@ -1,5 +1,16 @@ import React from "react"; -import { Box, Heading, Text, Badge, IconButton } from "@chakra-ui/core"; +import { + Box, + Heading, + Text, + Badge, + IconButton, + Slider, + SliderTrack, + SliderFilledTrack, + SliderThumb +} from "@chakra-ui/core"; + import { FaPauseCircle, FaArrowAltCircleUp, @@ -7,13 +18,31 @@ import { } from "react-icons/fa"; import axios from "axios"; +function PositionSelector({ position, id }) { + const doAction = value => { + axios.get(`/roller/${id}/position/${value}`); + }; + + if (!position) { + return ""; + } + return ( + + + + + + + + ); +} + function BlindItem(data) { - const { name, id, online = false, action = "stop" } = data; + const { name, id, online = false, action = "stop", position } = data; const doAction = ({ id, action }) => { axios.get(`/roller/${id}/${action}`); }; - return ( {name} @@ -28,6 +57,12 @@ function BlindItem(data) { Current action: {action} + + + Current position: {position ? `${position}%` : "..."} + + +