2018-03-13 18:26:12 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2018-03-18 18:25:06 +01:00
|
|
|
"""
|
|
|
|
Doorlockd -- Binary Kitchen's smart door opener
|
|
|
|
|
|
|
|
Copyright (c) Binary Kitchen e.V., 2018
|
|
|
|
|
|
|
|
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-03-13 18:26:12 +01:00
|
|
|
import logging
|
|
|
|
import sys
|
|
|
|
|
2018-10-08 02:40:36 +02:00
|
|
|
from configparser import ConfigParser
|
2018-03-13 18:26:12 +01:00
|
|
|
from enum import Enum
|
2018-10-08 03:11:24 +02:00
|
|
|
from os.path import abspath, join
|
2018-03-13 18:26:12 +01:00
|
|
|
from serial import Serial
|
2018-03-18 22:03:48 +01:00
|
|
|
from subprocess import Popen
|
2018-03-18 15:48:59 +01:00
|
|
|
from threading import Thread
|
|
|
|
from time import sleep
|
2018-03-13 18:26:12 +01:00
|
|
|
|
2018-10-08 01:12:32 +02:00
|
|
|
from pydoorlock.Authenticator import Authenticator, AuthMethod, AuthenticationResult
|
2018-10-08 03:11:24 +02:00
|
|
|
from pydoorlock.WebApp import webapp_run, emit_doorstate
|
2018-10-08 01:55:42 +02:00
|
|
|
from pydoorlock.Door import DoorState
|
2018-10-08 01:12:32 +02:00
|
|
|
|
2018-09-05 21:26:38 +02:00
|
|
|
SYSCONFDIR = '.'
|
|
|
|
PREFIX = '.'
|
|
|
|
|
2018-09-05 21:29:12 +02:00
|
|
|
root_prefix = join(PREFIX, 'share', 'doorlockd')
|
|
|
|
sounds_prefix = join(root_prefix, 'sounds')
|
2018-09-05 23:47:42 +02:00
|
|
|
scripts_prefix = join(root_prefix, 'scripts')
|
2018-09-05 23:14:28 +02:00
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
static_folder = abspath(join(root_prefix, 'static'))
|
|
|
|
template_folder = abspath(join(root_prefix, 'templates'))
|
|
|
|
|
2018-03-13 18:26:12 +01:00
|
|
|
__author__ = 'Ralf Ramsauer'
|
|
|
|
__copyright = 'Copyright (c) Ralf Ramsauer, 2018'
|
|
|
|
__license__ = 'GPLv2'
|
|
|
|
__email__ = 'ralf@binary-kitchen.de'
|
|
|
|
__status__ = 'Development'
|
|
|
|
__maintainer__ = 'Ralf Ramsauer'
|
2018-10-07 03:48:13 +02:00
|
|
|
__version__ = '2.0-rc2'
|
2018-03-13 18:26:12 +01:00
|
|
|
|
|
|
|
log_level = logging.DEBUG
|
|
|
|
date_fmt = '%Y-%m-%d %H:%M:%S'
|
|
|
|
log_fmt = '%(asctime)-15s %(levelname)-8s %(message)s'
|
|
|
|
log = logging.getLogger()
|
|
|
|
|
2018-10-08 02:40:36 +02:00
|
|
|
class Config:
|
|
|
|
config_topic = 'doorlock'
|
|
|
|
|
|
|
|
def __init__(self, sysconfdir):
|
|
|
|
self.config = ConfigParser()
|
|
|
|
self.config.read([join(sysconfdir, 'doorlockd.default.cfg'),
|
|
|
|
join(sysconfdir, 'doorlockd.cfg')])
|
|
|
|
|
|
|
|
def boolean(self, key):
|
|
|
|
return self.config.getboolean(self.config_topic, key)
|
|
|
|
|
|
|
|
def str(self, key):
|
|
|
|
return self.config.get(self.config_topic, key)
|
|
|
|
|
|
|
|
cfg = Config(SYSCONFDIR)
|
|
|
|
|
|
|
|
# Booleans
|
|
|
|
run_hooks = cfg.boolean('RUN_HOOKS')
|
|
|
|
sounds = cfg.boolean('SOUNDS')
|
2018-03-13 18:26:12 +01:00
|
|
|
|
2018-09-05 21:29:12 +02:00
|
|
|
wave_emergency = 'emergency_unlock.wav'
|
2018-10-07 03:47:05 +02:00
|
|
|
|
2018-09-05 21:29:12 +02:00
|
|
|
wave_lock = 'lock.wav'
|
|
|
|
wave_lock_button = 'lock_button.wav'
|
2018-10-07 03:47:05 +02:00
|
|
|
|
|
|
|
wave_present = 'present.wav'
|
|
|
|
wave_present_button = 'present.wav'
|
|
|
|
|
2018-09-05 21:29:12 +02:00
|
|
|
wave_unlock = 'unlock.wav'
|
|
|
|
wave_unlock_button = 'unlock_button.wav'
|
2018-10-07 03:47:05 +02:00
|
|
|
|
2018-09-05 21:29:12 +02:00
|
|
|
wave_zonk = 'zonk.wav'
|
|
|
|
|
2018-03-23 23:00:25 +01:00
|
|
|
|
|
|
|
def playsound(filename):
|
|
|
|
if not sounds:
|
|
|
|
return
|
2018-09-05 21:29:12 +02:00
|
|
|
Popen(['nohup', 'aplay', join(sounds_prefix, filename)])
|
2018-03-23 23:00:25 +01:00
|
|
|
|
|
|
|
|
2018-03-18 22:03:48 +01:00
|
|
|
def start_hook(script):
|
2018-03-21 00:58:16 +01:00
|
|
|
if not run_hooks:
|
|
|
|
log.info('Hooks disabled: not starting %s' % script)
|
2018-03-18 22:03:48 +01:00
|
|
|
return
|
|
|
|
log.info('Starting hook %s' % script)
|
2018-09-05 23:47:42 +02:00
|
|
|
Popen(['nohup', join(scripts_prefix, script)])
|
2018-03-18 22:03:48 +01:00
|
|
|
|
|
|
|
|
2018-10-07 03:47:05 +02:00
|
|
|
def sound_helper(old_state, new_state, button):
|
|
|
|
if old_state == new_state:
|
|
|
|
playsound(wave_zonk)
|
|
|
|
|
|
|
|
if button:
|
|
|
|
if new_state == DoorState.Open:
|
|
|
|
playsound(wave_unlock_button)
|
|
|
|
elif new_state == DoorState.Present:
|
|
|
|
playsound(wave_present_button)
|
|
|
|
elif new_state == DoorState.Closed:
|
|
|
|
playsound(wave_lock_button)
|
|
|
|
else:
|
|
|
|
if new_state == DoorState.Open:
|
|
|
|
playsound(wave_unlock)
|
|
|
|
elif new_state == DoorState.Present:
|
|
|
|
playsound(wave_present)
|
|
|
|
elif new_state == DoorState.Closed:
|
|
|
|
playsound(wave_lock)
|
|
|
|
|
|
|
|
|
2018-03-13 18:26:12 +01:00
|
|
|
class LogicResponse(Enum):
|
|
|
|
Success = 0
|
|
|
|
Perm = 1
|
2018-09-03 00:29:30 +02:00
|
|
|
AlreadyActive = 2
|
|
|
|
# don't break old apps, value 3 is reserved now
|
|
|
|
RESERVED = 3
|
2018-03-13 18:26:12 +01:00
|
|
|
Inval = 4
|
|
|
|
|
2018-03-19 18:46:33 +01:00
|
|
|
EmergencyUnlock = 10,
|
|
|
|
ButtonLock = 11,
|
|
|
|
ButtonUnlock = 12,
|
2018-09-02 04:11:15 +02:00
|
|
|
ButtonPresent = 13,
|
2018-03-19 18:46:33 +01:00
|
|
|
|
2018-10-08 01:12:32 +02:00
|
|
|
def __str__(self):
|
2018-03-13 18:26:12 +01:00
|
|
|
if self == LogicResponse.Success:
|
|
|
|
return 'Yo, passt.'
|
|
|
|
elif self == LogicResponse.Perm:
|
|
|
|
return choose_insult()
|
2018-09-03 00:29:30 +02:00
|
|
|
elif self == LogicResponse.AlreadyActive:
|
|
|
|
return 'Zustand bereits aktiv'
|
2018-03-13 18:26:12 +01:00
|
|
|
elif self == LogicResponse.Inval:
|
|
|
|
return 'Das was du willst geht nicht.'
|
2018-03-19 18:46:33 +01:00
|
|
|
elif self == LogicResponse.EmergencyUnlock:
|
|
|
|
return '!!! Emergency Unlock !!!'
|
|
|
|
elif self == LogicResponse.ButtonLock:
|
2018-09-02 04:11:15 +02:00
|
|
|
return 'Closed by button'
|
2018-03-19 18:46:33 +01:00
|
|
|
elif self == LogicResponse.ButtonUnlock:
|
2018-09-02 04:11:15 +02:00
|
|
|
return 'Opened by button'
|
|
|
|
elif self == LogicResponse.ButtonPresent:
|
|
|
|
return 'Present by button'
|
2018-03-13 18:26:12 +01:00
|
|
|
|
2018-10-08 01:12:32 +02:00
|
|
|
return 'Error'
|
2018-03-13 18:26:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
class DoorHandler:
|
2018-09-02 04:11:15 +02:00
|
|
|
state = DoorState.Closed
|
|
|
|
do_close = False
|
|
|
|
|
|
|
|
CMD_PRESENT = b'y'
|
|
|
|
CMD_OPEN = b'g'
|
|
|
|
CMD_CLOSE = b'r'
|
2018-03-13 18:26:12 +01:00
|
|
|
|
2018-09-02 04:11:15 +02:00
|
|
|
BUTTON_PRESENT = b'Y'
|
|
|
|
BUTTON_OPEN = b'G'
|
|
|
|
BUTTON_CLOSE = b'R'
|
2018-09-04 01:05:46 +02:00
|
|
|
|
|
|
|
CMD_EMERGENCY_SWITCH = b'E'
|
2018-09-02 04:11:15 +02:00
|
|
|
# TBD DOOR NOT CLOSED
|
2018-03-19 18:46:33 +01:00
|
|
|
|
2018-03-13 18:26:12 +01:00
|
|
|
def __init__(self, device):
|
2018-10-08 22:14:25 +02:00
|
|
|
if cfg.boolean('SIMULATE_SERIAL'):
|
2018-03-16 16:52:43 +01:00
|
|
|
return
|
|
|
|
|
2018-10-08 22:14:25 +02:00
|
|
|
device = cfg.str('SERIAL_PORT')
|
|
|
|
log.info('Using serial port: %s' % device)
|
|
|
|
|
2018-03-13 18:26:12 +01:00
|
|
|
self.serial = Serial(device, baudrate=9600, bytesize=8, parity='N',
|
2018-03-19 18:46:33 +01:00
|
|
|
stopbits=1, timeout=0)
|
2018-03-13 18:26:12 +01:00
|
|
|
self.thread = Thread(target=self.thread_worker)
|
2018-09-06 00:49:23 +02:00
|
|
|
log.debug('Spawning RS232 Thread')
|
2018-03-13 18:26:12 +01:00
|
|
|
self.thread.start()
|
|
|
|
|
|
|
|
def thread_worker(self):
|
|
|
|
while True:
|
2018-03-19 18:46:33 +01:00
|
|
|
sleep(0.4)
|
|
|
|
while True:
|
2018-09-06 00:49:23 +02:00
|
|
|
rx = self.serial.read(1)
|
|
|
|
if len(rx) == 0:
|
2018-03-19 18:46:33 +01:00
|
|
|
break
|
2018-10-07 03:47:05 +02:00
|
|
|
|
|
|
|
old_state = self.state
|
2018-09-06 00:49:23 +02:00
|
|
|
if rx == DoorHandler.BUTTON_CLOSE:
|
|
|
|
self.close()
|
|
|
|
log.info('Closed due to Button press')
|
2018-10-08 03:11:24 +02:00
|
|
|
#emit_status(LogicResponse.ButtonLock)
|
2018-09-06 00:49:23 +02:00
|
|
|
elif rx == DoorHandler.BUTTON_OPEN:
|
|
|
|
self.open()
|
|
|
|
log.info('Opened due to Button press')
|
2018-10-08 03:11:24 +02:00
|
|
|
#emit_status(LogicResponse.ButtonUnlock)
|
2018-09-06 00:49:23 +02:00
|
|
|
elif rx == DoorHandler.BUTTON_PRESENT:
|
|
|
|
self.present()
|
|
|
|
log.info('Present due to Button press')
|
2018-10-08 03:11:24 +02:00
|
|
|
#emit_status(LogicResponse.ButtonPresent)
|
2018-09-06 00:49:23 +02:00
|
|
|
elif rx == DoorHandler.CMD_EMERGENCY_SWITCH:
|
|
|
|
log.warning('Emergency unlock')
|
2018-10-08 03:11:24 +02:00
|
|
|
#emit_status(LogicResponse.EmergencyUnlock)
|
2018-09-06 00:49:23 +02:00
|
|
|
else:
|
|
|
|
log.error('Received unknown message "%s" from AVR' % rx)
|
2018-03-13 18:26:12 +01:00
|
|
|
|
2018-10-07 03:47:05 +02:00
|
|
|
sound_helper(old_state, self.state, True)
|
|
|
|
|
2018-09-02 04:11:15 +02:00
|
|
|
if self.do_close:
|
2018-09-06 00:49:23 +02:00
|
|
|
tx = DoorHandler.CMD_CLOSE
|
2018-09-02 04:11:15 +02:00
|
|
|
self.do_close = False
|
2018-09-08 00:58:32 +02:00
|
|
|
elif self.state == DoorState.Present:
|
2018-09-06 00:49:23 +02:00
|
|
|
tx = DoorHandler.CMD_PRESENT
|
|
|
|
elif self.state == DoorState.Open:
|
|
|
|
tx = DoorHandler.CMD_OPEN
|
|
|
|
else:
|
|
|
|
continue
|
|
|
|
|
|
|
|
self.serial.write(tx)
|
|
|
|
self.serial.flush()
|
2018-03-13 18:26:12 +01:00
|
|
|
|
|
|
|
def open(self):
|
|
|
|
if self.state == DoorState.Open:
|
2018-09-03 00:29:30 +02:00
|
|
|
return LogicResponse.AlreadyActive
|
2018-03-13 18:26:12 +01:00
|
|
|
|
|
|
|
self.state = DoorState.Open
|
2018-09-08 00:37:21 +02:00
|
|
|
start_hook('post_unlock')
|
2018-03-13 18:26:12 +01:00
|
|
|
return LogicResponse.Success
|
|
|
|
|
2018-09-08 00:37:21 +02:00
|
|
|
def present(self):
|
|
|
|
if self.state == DoorState.Present:
|
2018-09-03 00:29:30 +02:00
|
|
|
return LogicResponse.AlreadyActive
|
2018-03-13 18:26:12 +01:00
|
|
|
|
2018-09-08 00:37:21 +02:00
|
|
|
self.state = DoorState.Present
|
|
|
|
start_hook('post_present')
|
2018-03-13 18:26:12 +01:00
|
|
|
return LogicResponse.Success
|
|
|
|
|
2018-09-08 00:37:21 +02:00
|
|
|
def close(self):
|
|
|
|
if self.state == DoorState.Closed:
|
2018-09-03 00:29:30 +02:00
|
|
|
return LogicResponse.AlreadyActive
|
2018-09-02 04:11:15 +02:00
|
|
|
|
2018-09-08 00:37:21 +02:00
|
|
|
self.do_close = True
|
|
|
|
self.state = DoorState.Closed
|
|
|
|
start_hook('post_lock')
|
2018-09-02 04:11:15 +02:00
|
|
|
return LogicResponse.Success
|
|
|
|
|
2018-03-13 18:26:12 +01:00
|
|
|
def request(self, state):
|
2018-09-02 04:11:15 +02:00
|
|
|
if state == DoorState.Closed:
|
2018-10-08 03:11:24 +02:00
|
|
|
err = self.close()
|
2018-09-02 04:11:15 +02:00
|
|
|
elif state == DoorState.Present:
|
2018-10-08 03:11:24 +02:00
|
|
|
err = self.present()
|
2018-03-13 18:26:12 +01:00
|
|
|
elif state == DoorState.Open:
|
2018-10-08 03:11:24 +02:00
|
|
|
err = self.open()
|
|
|
|
emit_doorstate()
|
2018-03-13 18:26:12 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Logic:
|
2018-10-08 22:14:25 +02:00
|
|
|
def __init__(self, cfg):
|
2018-10-08 19:47:26 +02:00
|
|
|
self.auth = Authenticator(cfg)
|
2018-10-08 22:14:25 +02:00
|
|
|
self.door_handler = DoorHandler(cfg)
|
2018-03-13 18:26:12 +01:00
|
|
|
|
|
|
|
def _request(self, state, credentials):
|
2018-10-08 01:12:32 +02:00
|
|
|
err = self.auth.try_auth(credentials)
|
|
|
|
if err != AuthenticationResult.Success:
|
2018-03-13 18:26:12 +01:00
|
|
|
return err
|
|
|
|
return self.door_handler.request(state)
|
|
|
|
|
|
|
|
def request(self, state, credentials):
|
2018-10-07 03:47:05 +02:00
|
|
|
old_state = self.door_handler.state
|
2018-03-13 18:26:12 +01:00
|
|
|
err = self._request(state, credentials)
|
2018-10-07 03:47:05 +02:00
|
|
|
if err == LogicResponse.Success or err == LogicResponse.AlreadyActive:
|
|
|
|
sound_helper(old_state, self.door_handler.state, False)
|
2018-03-13 18:26:12 +01:00
|
|
|
return err
|
|
|
|
|
|
|
|
@property
|
|
|
|
def state(self):
|
|
|
|
return self.door_handler.state
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
logging.basicConfig(level=log_level, stream=sys.stdout,
|
|
|
|
format=log_fmt, datefmt=date_fmt)
|
|
|
|
log.info('Starting doorlockd')
|
|
|
|
|
2018-10-08 22:14:25 +02:00
|
|
|
logic = Logic(cfg)
|
2018-03-13 18:26:12 +01:00
|
|
|
|
2018-10-08 03:11:24 +02:00
|
|
|
webapp_run(cfg, logic, __status__, __version__, template_folder,
|
|
|
|
static_folder)
|
2018-03-13 18:26:12 +01:00
|
|
|
|
|
|
|
sys.exit(0)
|