2018-10-09 01:42:16 +02:00
|
|
|
"""
|
|
|
|
Doorlockd -- Binary Kitchen's smart door opener
|
|
|
|
|
2018-10-17 19:16:37 +02:00
|
|
|
Copyright (c) Binary Kitchen e.V., 2020
|
2018-10-09 01:42:16 +02:00
|
|
|
|
|
|
|
Author:
|
|
|
|
Ralf Ramsauer <ralf@binary-kitchen.de>
|
|
|
|
|
|
|
|
This work is licensed under the terms of the GNU GPL, version 2. See
|
|
|
|
the LICENSE file in the top-level directory.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful, but WITHOUT
|
|
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
|
|
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
|
|
|
|
details.
|
|
|
|
"""
|
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
import logging
|
2018-10-17 19:16:37 +02:00
|
|
|
import json
|
|
|
|
import threading
|
2018-10-08 03:11:24 +02:00
|
|
|
|
2018-10-17 19:16:37 +02:00
|
|
|
from flask import abort, Flask, jsonify, render_template, request, Response
|
2018-10-08 01:56:59 +02:00
|
|
|
from flask_wtf import FlaskForm
|
|
|
|
from wtforms import PasswordField, StringField, SubmitField
|
|
|
|
from wtforms.validators import DataRequired, Length
|
|
|
|
|
|
|
|
from .Door import DoorState
|
2018-10-09 01:46:09 +02:00
|
|
|
from .Doorlock import DoorlockResponse
|
2018-10-08 01:56:59 +02:00
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
log = logging.getLogger()
|
|
|
|
webapp = Flask(__name__)
|
2018-10-17 19:16:37 +02:00
|
|
|
evt = threading.Event()
|
|
|
|
json_push_state = ""
|
2018-10-08 03:11:24 +02:00
|
|
|
|
|
|
|
|
2018-10-09 01:06:59 +02:00
|
|
|
def emit_doorstate(response=None):
|
2018-10-17 19:16:37 +02:00
|
|
|
global json_push_state
|
|
|
|
|
2018-10-09 01:06:59 +02:00
|
|
|
if response:
|
|
|
|
message = str(response)
|
|
|
|
else:
|
2018-10-17 19:16:37 +02:00
|
|
|
message = str(logic.state)
|
|
|
|
|
|
|
|
json_push_state = json.dumps({
|
2021-10-18 18:09:52 +02:00
|
|
|
'message': message,
|
|
|
|
'img': logic.state.to_img()})
|
2018-10-17 19:16:37 +02:00
|
|
|
|
2021-10-18 18:09:52 +02:00
|
|
|
# Notify push clients
|
2018-10-17 19:16:37 +02:00
|
|
|
evt.set()
|
|
|
|
evt.clear()
|
|
|
|
|
2021-10-18 18:09:52 +02:00
|
|
|
|
2018-10-17 19:16:37 +02:00
|
|
|
def push_state():
|
|
|
|
def event_str():
|
|
|
|
return "data: {}\n\n".format(json_push_state)
|
|
|
|
|
|
|
|
try:
|
|
|
|
yield event_str()
|
|
|
|
while True:
|
|
|
|
evt.wait()
|
|
|
|
yield event_str()
|
|
|
|
except GeneratorExit:
|
|
|
|
return
|
2018-10-08 03:11:24 +02:00
|
|
|
|
2021-10-18 18:09:52 +02:00
|
|
|
|
2018-10-08 01:56:59 +02:00
|
|
|
class AuthenticationForm(FlaskForm):
|
|
|
|
username = StringField('Username', [Length(min=3, max=25)])
|
|
|
|
password = PasswordField('Password', [DataRequired()])
|
|
|
|
open = SubmitField('Open')
|
|
|
|
present = SubmitField('Present')
|
|
|
|
close = SubmitField('Close')
|
|
|
|
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
FlaskForm.__init__(self, *args, **kwargs)
|
|
|
|
self.desired_state = DoorState.Closed
|
|
|
|
|
|
|
|
def validate(self):
|
|
|
|
if not FlaskForm.validate(self):
|
|
|
|
return False
|
|
|
|
|
|
|
|
if self.open.data:
|
|
|
|
self.desired_state = DoorState.Open
|
|
|
|
elif self.present.data:
|
|
|
|
self.desired_state = DoorState.Present
|
|
|
|
|
|
|
|
return True
|
2018-10-08 03:11:24 +02:00
|
|
|
|
2021-10-18 18:09:52 +02:00
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
@webapp.route('/display')
|
|
|
|
def display():
|
|
|
|
return render_template('display.html',
|
|
|
|
room=room,
|
|
|
|
title=title,
|
|
|
|
welcome=welcome)
|
|
|
|
|
2021-10-18 18:09:52 +02:00
|
|
|
|
2018-10-17 19:16:37 +02:00
|
|
|
@webapp.route('/push')
|
|
|
|
def push():
|
|
|
|
emit_doorstate()
|
2021-10-18 18:09:52 +02:00
|
|
|
resp = Response(push_state(), mimetype="text/event-stream")
|
2018-10-17 19:16:37 +02:00
|
|
|
resp.headers['X-Accel-Buffering'] = 'no'
|
|
|
|
resp.headers['Cache-Control'] = 'no-cache'
|
|
|
|
return resp
|
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
|
|
|
|
@webapp.route('/api', methods=['POST'])
|
|
|
|
def api():
|
|
|
|
def json_response(response, msg=None):
|
2018-10-17 19:16:37 +02:00
|
|
|
json_dict = dict()
|
|
|
|
json_dict['err'] = response.value
|
|
|
|
json_dict['msg'] = str(response) if msg is None else msg
|
2018-10-09 01:46:09 +02:00
|
|
|
if response == DoorlockResponse.Success or \
|
|
|
|
response == DoorlockResponse.AlreadyActive:
|
2018-10-08 03:11:24 +02:00
|
|
|
# TBD: Remove 'open'. No more users. Still used in App Version 2.1.1!
|
2018-10-17 19:16:37 +02:00
|
|
|
json_dict['open'] = logic.state.is_open()
|
|
|
|
json_dict['status'] = logic.state.value
|
|
|
|
return jsonify(json_dict)
|
2018-10-08 03:11:24 +02:00
|
|
|
|
|
|
|
user = request.form.get('user')
|
|
|
|
password = request.form.get('pass')
|
|
|
|
command = request.form.get('command')
|
|
|
|
|
|
|
|
if any(v is None for v in [user, password, command]):
|
|
|
|
log.warning('Incomplete API request')
|
|
|
|
abort(400)
|
|
|
|
|
|
|
|
if not user or not password:
|
|
|
|
log.warning('Invalid username or password format')
|
2018-10-09 01:46:09 +02:00
|
|
|
return json_response(DoorlockResponse.Inval,
|
2018-10-08 03:11:24 +02:00
|
|
|
'Invalid username or password format')
|
|
|
|
|
2018-11-27 08:40:23 +01:00
|
|
|
credentials = user, password
|
2018-10-08 03:11:24 +02:00
|
|
|
|
|
|
|
if command == 'status':
|
2018-10-09 01:46:09 +02:00
|
|
|
return json_response(logic.auth.try_auth(credentials))
|
2018-10-08 03:11:24 +02:00
|
|
|
|
|
|
|
desired_state = DoorState.from_string(command)
|
|
|
|
if not desired_state:
|
2018-10-09 01:46:09 +02:00
|
|
|
return json_response(DoorlockResponse.Inval, "Invalid command requested")
|
2018-10-08 03:11:24 +02:00
|
|
|
|
|
|
|
log.info('Incoming API request from %s' % user.encode('utf-8'))
|
|
|
|
log.info(' desired state: %s' % desired_state)
|
|
|
|
log.info(' current state: %s' % logic.state)
|
|
|
|
|
|
|
|
response = logic.request(desired_state, credentials)
|
|
|
|
|
|
|
|
return json_response(response)
|
|
|
|
|
|
|
|
|
|
|
|
@webapp.route('/', methods=['GET', 'POST'])
|
|
|
|
def home():
|
|
|
|
authentication_form = AuthenticationForm()
|
|
|
|
response = None
|
|
|
|
|
|
|
|
if request.method == 'POST' and authentication_form.validate():
|
|
|
|
user = authentication_form.username.data
|
|
|
|
password = authentication_form.password.data
|
2018-11-27 08:40:23 +01:00
|
|
|
credentials = user, password
|
2018-10-08 03:11:24 +02:00
|
|
|
|
|
|
|
log.info('Incoming request from %s' % user.encode('utf-8'))
|
|
|
|
desired_state = authentication_form.desired_state
|
|
|
|
log.info(' desired state: %s' % desired_state)
|
|
|
|
log.info(' current state: %s' % logic.state)
|
2018-10-17 19:16:37 +02:00
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
response = logic.request(desired_state, credentials)
|
|
|
|
log.info(' response: %s' % response)
|
|
|
|
|
|
|
|
# Don't trust python, zero credentials
|
|
|
|
user = password = credentials = None
|
|
|
|
|
|
|
|
return render_template('index.html',
|
|
|
|
authentication_form=authentication_form,
|
|
|
|
response=response,
|
|
|
|
state_text=str(logic.state),
|
|
|
|
led=logic.state.to_img(),
|
|
|
|
banner='%s - %s' % (title, room))
|
|
|
|
|
2018-10-09 01:38:39 +02:00
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
def webapp_run(cfg, my_logic, status, version, template_folder, static_folder):
|
|
|
|
global logic
|
|
|
|
logic = my_logic
|
2018-10-17 19:16:37 +02:00
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
debug = cfg.boolean('DEBUG')
|
|
|
|
|
|
|
|
host = 'localhost'
|
|
|
|
if debug:
|
|
|
|
host = '0.0.0.0'
|
|
|
|
|
|
|
|
global room
|
|
|
|
room = cfg.str('ROOM')
|
|
|
|
|
|
|
|
global title
|
|
|
|
title = cfg.str('TITLE')
|
|
|
|
|
|
|
|
global welcome
|
|
|
|
welcome = cfg.str('WELCOME')
|
|
|
|
|
|
|
|
global html_title
|
|
|
|
html_title = '%s (%s - v%s)' % (title, status, version)
|
|
|
|
|
|
|
|
webapp.config['SECRET_KEY'] = cfg.str('SECRET_KEY')
|
|
|
|
webapp.template_folder = template_folder
|
|
|
|
webapp.static_folder = static_folder
|
2018-10-17 19:16:37 +02:00
|
|
|
webapp.debug = debug
|
2021-10-18 18:09:52 +02:00
|
|
|
webapp.run(host=host, port=8080)
|