mirror of
https://github.com/binary-kitchen/doorlockd
synced 2024-12-22 10:24:26 +01:00
pydoorlock: WebApp: outsource WebApp
Signed-off-by: Ralf Ramsauer <ralf@binary-kitchen.de>
This commit is contained in:
parent
2cdf9d1b27
commit
5b296b777d
154
doorlockd
154
doorlockd
@ -22,20 +22,14 @@ import sys
|
|||||||
|
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from os.path import join
|
from os.path import abspath, join
|
||||||
from serial import Serial
|
from serial import Serial
|
||||||
from subprocess import Popen
|
from subprocess import Popen
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from flask import abort, Flask, jsonify, render_template, request
|
|
||||||
from flask_socketio import SocketIO
|
|
||||||
from flask_wtf import FlaskForm
|
|
||||||
from wtforms import PasswordField, StringField, SubmitField
|
|
||||||
from wtforms.validators import DataRequired, Length
|
|
||||||
|
|
||||||
from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult
|
from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult
|
||||||
from pydoorlock.WebApp import AuthenticationForm
|
from pydoorlock.WebApp import webapp_run, emit_doorstate
|
||||||
from pydoorlock.Door import DoorState
|
from pydoorlock.Door import DoorState
|
||||||
|
|
||||||
SYSCONFDIR = '.'
|
SYSCONFDIR = '.'
|
||||||
@ -43,10 +37,11 @@ PREFIX = '.'
|
|||||||
|
|
||||||
root_prefix = join(PREFIX, 'share', 'doorlockd')
|
root_prefix = join(PREFIX, 'share', 'doorlockd')
|
||||||
sounds_prefix = join(root_prefix, 'sounds')
|
sounds_prefix = join(root_prefix, 'sounds')
|
||||||
static_folder = join(root_prefix, 'static')
|
|
||||||
template_folder = join(root_prefix, 'templates')
|
|
||||||
scripts_prefix = join(root_prefix, 'scripts')
|
scripts_prefix = join(root_prefix, 'scripts')
|
||||||
|
|
||||||
|
static_folder = abspath(join(root_prefix, 'static'))
|
||||||
|
template_folder = abspath(join(root_prefix, 'templates'))
|
||||||
|
|
||||||
__author__ = 'Ralf Ramsauer'
|
__author__ = 'Ralf Ramsauer'
|
||||||
__copyright = 'Copyright (c) Ralf Ramsauer, 2018'
|
__copyright = 'Copyright (c) Ralf Ramsauer, 2018'
|
||||||
__license__ = 'GPLv2'
|
__license__ = 'GPLv2'
|
||||||
@ -60,9 +55,6 @@ date_fmt = '%Y-%m-%d %H:%M:%S'
|
|||||||
log_fmt = '%(asctime)-15s %(levelname)-8s %(message)s'
|
log_fmt = '%(asctime)-15s %(levelname)-8s %(message)s'
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
webapp = Flask(__name__)
|
|
||||||
socketio = SocketIO(webapp, async_mode='threading')
|
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
config_topic = 'doorlock'
|
config_topic = 'doorlock'
|
||||||
|
|
||||||
@ -80,17 +72,11 @@ class Config:
|
|||||||
cfg = Config(SYSCONFDIR)
|
cfg = Config(SYSCONFDIR)
|
||||||
|
|
||||||
# Booleans
|
# Booleans
|
||||||
debug = cfg.boolean('DEBUG')
|
|
||||||
simulate_serial = cfg.boolean('SIMULATE_SERIAL')
|
simulate_serial = cfg.boolean('SIMULATE_SERIAL')
|
||||||
run_hooks = cfg.boolean('RUN_HOOKS')
|
run_hooks = cfg.boolean('RUN_HOOKS')
|
||||||
sounds = cfg.boolean('SOUNDS')
|
sounds = cfg.boolean('SOUNDS')
|
||||||
|
|
||||||
serial_port = cfg.str('SERIAL_PORT')
|
serial_port = cfg.str('SERIAL_PORT')
|
||||||
room = cfg.str('ROOM')
|
|
||||||
title = cfg.str('TITLE')
|
|
||||||
welcome = cfg.str('WELCOME')
|
|
||||||
|
|
||||||
html_title = '%s (%s - v%s)' % (title, __status__, __version__)
|
|
||||||
|
|
||||||
wave_emergency = 'emergency_unlock.wav'
|
wave_emergency = 'emergency_unlock.wav'
|
||||||
|
|
||||||
@ -211,18 +197,18 @@ class DoorHandler:
|
|||||||
if rx == DoorHandler.BUTTON_CLOSE:
|
if rx == DoorHandler.BUTTON_CLOSE:
|
||||||
self.close()
|
self.close()
|
||||||
log.info('Closed due to Button press')
|
log.info('Closed due to Button press')
|
||||||
logic.emit_status(LogicResponse.ButtonLock)
|
#emit_status(LogicResponse.ButtonLock)
|
||||||
elif rx == DoorHandler.BUTTON_OPEN:
|
elif rx == DoorHandler.BUTTON_OPEN:
|
||||||
self.open()
|
self.open()
|
||||||
log.info('Opened due to Button press')
|
log.info('Opened due to Button press')
|
||||||
logic.emit_status(LogicResponse.ButtonUnlock)
|
#emit_status(LogicResponse.ButtonUnlock)
|
||||||
elif rx == DoorHandler.BUTTON_PRESENT:
|
elif rx == DoorHandler.BUTTON_PRESENT:
|
||||||
self.present()
|
self.present()
|
||||||
log.info('Present due to Button press')
|
log.info('Present due to Button press')
|
||||||
logic.emit_status(LogicResponse.ButtonPresent)
|
#emit_status(LogicResponse.ButtonPresent)
|
||||||
elif rx == DoorHandler.CMD_EMERGENCY_SWITCH:
|
elif rx == DoorHandler.CMD_EMERGENCY_SWITCH:
|
||||||
log.warning('Emergency unlock')
|
log.warning('Emergency unlock')
|
||||||
logic.emit_status(LogicResponse.EmergencyUnlock)
|
#emit_status(LogicResponse.EmergencyUnlock)
|
||||||
else:
|
else:
|
||||||
log.error('Received unknown message "%s" from AVR' % rx)
|
log.error('Received unknown message "%s" from AVR' % rx)
|
||||||
|
|
||||||
@ -268,11 +254,12 @@ class DoorHandler:
|
|||||||
|
|
||||||
def request(self, state):
|
def request(self, state):
|
||||||
if state == DoorState.Closed:
|
if state == DoorState.Closed:
|
||||||
return self.close()
|
err = self.close()
|
||||||
elif state == DoorState.Present:
|
elif state == DoorState.Present:
|
||||||
return self.present()
|
err = self.present()
|
||||||
elif state == DoorState.Open:
|
elif state == DoorState.Open:
|
||||||
return self.open()
|
err = self.open()
|
||||||
|
emit_doorstate()
|
||||||
|
|
||||||
|
|
||||||
class Logic:
|
class Logic:
|
||||||
@ -291,118 +278,13 @@ class Logic:
|
|||||||
err = self._request(state, credentials)
|
err = self._request(state, credentials)
|
||||||
if err == LogicResponse.Success or err == LogicResponse.AlreadyActive:
|
if err == LogicResponse.Success or err == LogicResponse.AlreadyActive:
|
||||||
sound_helper(old_state, self.door_handler.state, False)
|
sound_helper(old_state, self.door_handler.state, False)
|
||||||
self.emit_status(err)
|
|
||||||
return err
|
return err
|
||||||
|
|
||||||
def emit_status(self, message=None):
|
|
||||||
led = self.state.to_img()
|
|
||||||
if message is None:
|
|
||||||
message = self.state.to_html()
|
|
||||||
else:
|
|
||||||
message = str(message)
|
|
||||||
|
|
||||||
socketio.emit('status', {'led': led, 'message': message})
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
return self.door_handler.state
|
return self.door_handler.state
|
||||||
|
|
||||||
|
|
||||||
@socketio.on('request_status')
|
|
||||||
@socketio.on('connect')
|
|
||||||
def on_connect():
|
|
||||||
logic.emit_status()
|
|
||||||
|
|
||||||
|
|
||||||
@webapp.route('/display')
|
|
||||||
def display():
|
|
||||||
return render_template('display.html',
|
|
||||||
room=room,
|
|
||||||
title=title,
|
|
||||||
welcome=welcome)
|
|
||||||
|
|
||||||
|
|
||||||
@webapp.route('/api', methods=['POST'])
|
|
||||||
def api():
|
|
||||||
def json_response(response, msg=None):
|
|
||||||
json = dict()
|
|
||||||
json['err'] = response.value
|
|
||||||
json['msg'] = response.to_html() if msg is None else msg
|
|
||||||
if response == LogicResponse.Success or \
|
|
||||||
response == LogicResponse.AlreadyActive:
|
|
||||||
# TBD: Remove 'open'. No more users. Still used in App Version 2.1.1!
|
|
||||||
json['open'] = logic.state.is_open()
|
|
||||||
json['status'] = logic.state.value
|
|
||||||
return jsonify(json)
|
|
||||||
|
|
||||||
method = request.form.get('method')
|
|
||||||
user = request.form.get('user')
|
|
||||||
password = request.form.get('pass')
|
|
||||||
command = request.form.get('command')
|
|
||||||
|
|
||||||
if method == 'local':
|
|
||||||
method = AuthMethod.LOCAL_USER_DB
|
|
||||||
else: # 'ldap' or default
|
|
||||||
method = AuthMethod.LDAP_USER_PW
|
|
||||||
|
|
||||||
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')
|
|
||||||
return json_response(LogicResponse.Inval,
|
|
||||||
'Invalid username or password format')
|
|
||||||
|
|
||||||
credentials = method, user, password
|
|
||||||
|
|
||||||
if command == 'status':
|
|
||||||
return json_response(logic.try_auth(credentials))
|
|
||||||
|
|
||||||
desired_state = DoorState.from_string(command)
|
|
||||||
if not desired_state:
|
|
||||||
return json_response(LogicResponse.Inval, "Invalid command requested")
|
|
||||||
|
|
||||||
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
|
|
||||||
method = authentication_form.method
|
|
||||||
credentials = method, user, password
|
|
||||||
|
|
||||||
log.info('Incoming request from %s' % user.encode('utf-8'))
|
|
||||||
log.info(' authentication method: %s' % method)
|
|
||||||
desired_state = authentication_form.desired_state
|
|
||||||
log.info(' desired state: %s' % desired_state)
|
|
||||||
log.info(' current state: %s' % logic.state)
|
|
||||||
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,
|
|
||||||
auth_backends=logic.auth.backends,
|
|
||||||
response=response,
|
|
||||||
state_text=logic.state.to_html(),
|
|
||||||
led=logic.state.to_img(),
|
|
||||||
banner='%s - %s' % (title, room))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(level=log_level, stream=sys.stdout,
|
logging.basicConfig(level=log_level, stream=sys.stdout,
|
||||||
format=log_fmt, datefmt=date_fmt)
|
format=log_fmt, datefmt=date_fmt)
|
||||||
@ -411,13 +293,7 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
logic = Logic()
|
logic = Logic()
|
||||||
|
|
||||||
host = 'localhost'
|
webapp_run(cfg, logic, __status__, __version__, template_folder,
|
||||||
if debug:
|
static_folder)
|
||||||
host = '0.0.0.0'
|
|
||||||
|
|
||||||
webapp.config['SECRET_KEY'] = cfg.str('SECRET_KEY')
|
|
||||||
webapp.template_folder=template_folder
|
|
||||||
webapp.static_folder=static_folder
|
|
||||||
socketio.run(webapp, host=host, port=8080, use_reloader=False, debug=debug)
|
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
class DoorState(Enum):
|
class DoorState(Enum):
|
||||||
# These numbers are used by the App since version 3.0, do NOT change them
|
|
||||||
Open = 0
|
Open = 0
|
||||||
Present = 1
|
Present = 1
|
||||||
Closed = 2
|
Closed = 2
|
||||||
@ -29,7 +28,7 @@ class DoorState(Enum):
|
|||||||
led = 'green'
|
led = 'green'
|
||||||
return 'static/led-%s.png' % led
|
return 'static/led-%s.png' % led
|
||||||
|
|
||||||
def to_html(self):
|
def __str__(self):
|
||||||
if self == DoorState.Open:
|
if self == DoorState.Open:
|
||||||
return 'Offen'
|
return 'Offen'
|
||||||
elif self == DoorState.Present:
|
elif self == DoorState.Present:
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import abort, Flask, jsonify, render_template, request
|
||||||
|
from flask_socketio import SocketIO
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from wtforms import PasswordField, StringField, SubmitField
|
from wtforms import PasswordField, StringField, SubmitField
|
||||||
from wtforms.validators import DataRequired, Length
|
from wtforms.validators import DataRequired, Length
|
||||||
@ -5,6 +9,17 @@ from wtforms.validators import DataRequired, Length
|
|||||||
from .Door import DoorState
|
from .Door import DoorState
|
||||||
from .Authenticator import AuthMethod
|
from .Authenticator import AuthMethod
|
||||||
|
|
||||||
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
webapp = Flask(__name__)
|
||||||
|
socketio = SocketIO(webapp, async_mode='threading')
|
||||||
|
|
||||||
|
|
||||||
|
def emit_doorstate():
|
||||||
|
state = logic.state
|
||||||
|
socketio.emit('status', {'led': state.to_img(), 'message': str(state)})
|
||||||
|
|
||||||
|
|
||||||
class AuthenticationForm(FlaskForm):
|
class AuthenticationForm(FlaskForm):
|
||||||
username = StringField('Username', [Length(min=3, max=25)])
|
username = StringField('Username', [Length(min=3, max=25)])
|
||||||
password = PasswordField('Password', [DataRequired()])
|
password = PasswordField('Password', [DataRequired()])
|
||||||
@ -32,3 +47,125 @@ class AuthenticationForm(FlaskForm):
|
|||||||
self.method = AuthMethod.LDAP_USER_PW
|
self.method = AuthMethod.LDAP_USER_PW
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@socketio.on('request_status')
|
||||||
|
@socketio.on('connect')
|
||||||
|
def on_connect():
|
||||||
|
emit_doorstate()
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/display')
|
||||||
|
def display():
|
||||||
|
return render_template('display.html',
|
||||||
|
room=room,
|
||||||
|
title=title,
|
||||||
|
welcome=welcome)
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api', methods=['POST'])
|
||||||
|
def api():
|
||||||
|
def json_response(response, msg=None):
|
||||||
|
json = dict()
|
||||||
|
json['err'] = response.value
|
||||||
|
json['msg'] = response.to_html() if msg is None else msg
|
||||||
|
if response == LogicResponse.Success or \
|
||||||
|
response == LogicResponse.AlreadyActive:
|
||||||
|
# TBD: Remove 'open'. No more users. Still used in App Version 2.1.1!
|
||||||
|
json['open'] = logic.state.is_open()
|
||||||
|
json['status'] = logic.state.value
|
||||||
|
return jsonify(json)
|
||||||
|
|
||||||
|
method = request.form.get('method')
|
||||||
|
user = request.form.get('user')
|
||||||
|
password = request.form.get('pass')
|
||||||
|
command = request.form.get('command')
|
||||||
|
|
||||||
|
if method == 'local':
|
||||||
|
method = AuthMethod.LOCAL_USER_DB
|
||||||
|
else: # 'ldap' or default
|
||||||
|
method = AuthMethod.LDAP_USER_PW
|
||||||
|
|
||||||
|
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')
|
||||||
|
return json_response(LogicResponse.Inval,
|
||||||
|
'Invalid username or password format')
|
||||||
|
|
||||||
|
credentials = method, user, password
|
||||||
|
|
||||||
|
if command == 'status':
|
||||||
|
return json_response(logic.try_auth(credentials))
|
||||||
|
|
||||||
|
desired_state = DoorState.from_string(command)
|
||||||
|
if not desired_state:
|
||||||
|
return json_response(LogicResponse.Inval, "Invalid command requested")
|
||||||
|
|
||||||
|
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
|
||||||
|
method = authentication_form.method
|
||||||
|
credentials = method, user, password
|
||||||
|
|
||||||
|
log.info('Incoming request from %s' % user.encode('utf-8'))
|
||||||
|
log.info(' authentication method: %s' % method)
|
||||||
|
desired_state = authentication_form.desired_state
|
||||||
|
log.info(' desired state: %s' % desired_state)
|
||||||
|
log.info(' current state: %s' % logic.state)
|
||||||
|
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,
|
||||||
|
auth_backends=logic.auth.backends,
|
||||||
|
response=response,
|
||||||
|
state_text=str(logic.state),
|
||||||
|
led=logic.state.to_img(),
|
||||||
|
banner='%s - %s' % (title, room))
|
||||||
|
|
||||||
|
def webapp_run(cfg, my_logic, status, version, template_folder, static_folder):
|
||||||
|
global logic
|
||||||
|
logic = my_logic
|
||||||
|
|
||||||
|
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
|
||||||
|
socketio.run(webapp, host=host, port=8080, use_reloader=False, debug=debug)
|
||||||
|
Loading…
Reference in New Issue
Block a user