From 4825c7346e80a393b617e71e5f382a752a0fcdd9 Mon Sep 17 00:00:00 2001 From: Thomas Schmid Date: Wed, 16 Nov 2022 20:58:32 +0100 Subject: [PATCH] Refactor pydoorlock to support multiple backends Signed-off-by: Thomas Schmid --- etc/doorlockd.cfg | 5 ++ pydoorlock/AvrDoorlock.py | 74 +++++++++++++++++++++ pydoorlock/Doorlock.py | 111 ++++++++++++++------------------ pydoorlock/DoorlockBackend.py | 17 +++++ pydoorlock/SimulationBackend.py | 5 ++ 5 files changed, 150 insertions(+), 62 deletions(-) create mode 100644 pydoorlock/AvrDoorlock.py create mode 100644 pydoorlock/DoorlockBackend.py create mode 100644 pydoorlock/SimulationBackend.py diff --git a/etc/doorlockd.cfg b/etc/doorlockd.cfg index 6b322a8..fae3b38 100644 --- a/etc/doorlockd.cfg +++ b/etc/doorlockd.cfg @@ -4,6 +4,7 @@ DEBUG = False SIMULATE_SERIAL = False SIMULATE_AUTH = False RUN_HOOKS = True +SIMULATE_BACKEND = True SOUNDS = True # LDAP @@ -19,7 +20,11 @@ TITLE = Binary Kitchen Doorlock ROOM = Hauptraum WELCOME = Willkommen in der Binary Kitchen +[backend] +BACKEND_TYPE = avr SERIAL_PORT = /dev/ttyAMA0 +# Simulation Backend for testing +#BACKEND_TYPE = simulation SECRET_KEY = foobar diff --git a/pydoorlock/AvrDoorlock.py b/pydoorlock/AvrDoorlock.py new file mode 100644 index 0000000..c4f2d21 --- /dev/null +++ b/pydoorlock/AvrDoorlock.py @@ -0,0 +1,74 @@ +from .DoorlockBackend import DoorlockBackend +from .Protocol import Protocol +from .Door import DoorState +from serial import Serial +from time import sleep +import logging +import threading + +log = logging.getLogger(__name__) + +class AvrDoorlockBackend(DoorlockBackend): + def __init__(self, device): + self.serial = Serial(device, baudrate=9600, bytesize=8, parity='N', + stopbits=1, timeout=0) + self.do_close = False + self.do_open = False + self.do_present = False + + threading.Thread(target=self.thread_worker).start() + + def get_capabilities(self): + return [DoorState.Closed, DoorState.Open, DoorState.Present] + + def set_state(self, state): + if state == DoorState.Closed: + self.do_close = True + elif state == DoorState.Open: + self.do_open = True + elif state == DoorState.Present: + self.do_present = True + + def thread_worker(self): + while True: + sleep(0.4) + while True: + rx = self.serial.read(1) + if len(rx) == 0: + break + + old_state = self.state + if rx == Protocol.STATE_SWITCH_RED.value.upper(): + self.door_handler.close() + log.info('Closed due to Button press') + self.door_handler.invoke_callback(DoorlockResponse.ButtonLock) + elif rx == Protocol.STATE_SWITCH_GREEN.value.upper(): + self.door_handler.open() + log.info('Opened due to Button press') + self.door_handler.invoke_callback(DoorlockResponse.ButtonUnlock) + elif rx == Protocol.STATE_SWITCH_YELLOW.value.upper(): + self.door_handler.present() + log.info('Present due to Button press') + self.door_handler.invoke_callback(DoorlockResponse.ButtonPresent) + elif rx == Protocol.EMERGENCY.value: + log.warning('Emergency unlock') + self.door_handler.invoke_callback(DoorlockResponse.EmergencyUnlock) + else: + log.error('Received unknown message "%s" from AVR' % rx) + + self.sound_helper(old_state, self.state, True) + + if self.do_close: + tx = Protocol.STATE_SWITCH_RED.value + self.do_close = False + elif self.do_present: + tx = Protocol.STATE_SWITCH_YELLOW.value + self.do_present = False + elif self.do_open: + tx = Protocol.STATE_SWITCH_GREEN.value + self.do_open = False + else: + continue + + self.serial.write(tx) + self.serial.flush() diff --git a/pydoorlock/Doorlock.py b/pydoorlock/Doorlock.py index 17be03f..f7720c0 100644 --- a/pydoorlock/Doorlock.py +++ b/pydoorlock/Doorlock.py @@ -20,10 +20,13 @@ import logging from enum import Enum from random import sample from subprocess import run -from serial import Serial from threading import Thread from time import sleep from os.path import join +from pydoorlock.AvrDoorlock import AvrDoorlockBackend +from pydoorlock.SimulationBackend import SimulationBackend + +from .Config import Config from .Door import DoorState from .Protocol import Protocol @@ -68,6 +71,7 @@ class DoorlockResponse(Enum): # don't break old apps, value 3 is reserved now RESERVED = 3 Inval = 4 + BackendError = 5 EmergencyUnlock = 10, ButtonLock = 11, @@ -91,6 +95,8 @@ class DoorlockResponse(Enum): return 'Opened by button' elif self == DoorlockResponse.ButtonPresent: return 'Present by button' + elif self == DoorlockResponse.BackendError: + return "Backend Error" return 'Error' @@ -110,7 +116,7 @@ class DoorHandler: wave_zonk = 'zonk.wav' - def __init__(self, cfg, sounds_prefix, scripts_prefix): + def __init__(self, cfg: Config, sounds_prefix, scripts_prefix): self._callback = None self.sounds = cfg.boolean('SOUNDS') @@ -120,84 +126,65 @@ class DoorHandler: self.scripts_prefix = scripts_prefix self.run_hooks = cfg.boolean('RUN_HOOKS') - if cfg.boolean('SIMULATE_SERIAL'): + backend_type = cfg.str("BACKEND_TYPE", "backend") + print(backend_type) + if not backend_type: + log.error("No backend configured") + raise RuntimeError() + + if backend_type == "avr": + self.backend = AvrDoorlockBackend(self) + elif backend_type == "simulation": + self.backend = SimulationBackend(self) + else: + log.error(f"Unknown backend {backend_type}") + raise RuntimeError + + def state_changed(self, new_state): + if new_state == DoorState.Open: + self.run_hook('post_unlock') + elif new_state == DoorState.Present: + self.run_hook('post_present') + elif new_state == DoorState.Closed: + self.run_hook('post_lock') + else: return - device = cfg.str('SERIAL_PORT') - log.info('Using serial port: %s' % device) + self.state = new_state - self.serial = Serial(device, baudrate=9600, bytesize=8, parity='N', - stopbits=1, timeout=0) - self.thread = Thread(target=self.thread_worker) - log.debug('Spawning RS232 Thread') - self.thread.start() - - def thread_worker(self): - while True: - sleep(0.4) - while True: - rx = self.serial.read(1) - if len(rx) == 0: - break - - old_state = self.state - if rx == Protocol.STATE_SWITCH_RED.value.upper(): - self.close() - log.info('Closed due to Button press') - self.invoke_callback(DoorlockResponse.ButtonLock) - elif rx == Protocol.STATE_SWITCH_GREEN.value.upper(): - self.open() - log.info('Opened due to Button press') - self.invoke_callback(DoorlockResponse.ButtonUnlock) - elif rx == Protocol.STATE_SWITCH_YELLOW.value.upper(): - self.present() - log.info('Present due to Button press') - self.invoke_callback(DoorlockResponse.ButtonPresent) - elif rx == Protocol.EMERGENCY.value: - log.warning('Emergency unlock') - self.invoke_callback(DoorlockResponse.EmergencyUnlock) - else: - log.error('Received unknown message "%s" from AVR' % rx) - - self.sound_helper(old_state, self.state, True) - - if self.do_close: - tx = Protocol.STATE_SWITCH_RED.value - self.do_close = False - elif self.state == DoorState.Present: - tx = Protocol.STATE_SWITCH_YELLOW.value - elif self.state == DoorState.Open: - tx = Protocol.STATE_SWITCH_GREEN.value - else: - continue - - self.serial.write(tx) - self.serial.flush() def open(self): if self.state == DoorState.Open: return DoorlockResponse.AlreadyActive - self.state = DoorState.Open - self.run_hook('post_unlock') - return DoorlockResponse.Success + if self.backend.set_state(DoorState.Open): + self.state = DoorState.Open + self.run_hook('post_unlock') + return DoorlockResponse.Success + + return DoorlockResponse.BackendError def present(self): if self.state == DoorState.Present: return DoorlockResponse.AlreadyActive - self.state = DoorState.Present - self.run_hook('post_present') - return DoorlockResponse.Success + if self.backend.set_state(DoorState.Present): + self.state = DoorState.Present + self.run_hook('post_present') + return DoorlockResponse.Success + + return DoorlockResponse.BackendError def close(self): if self.state == DoorState.Closed: return DoorlockResponse.AlreadyActive - self.do_close = True - self.state = DoorState.Closed - self.run_hook('post_lock') - return DoorlockResponse.Success + if self.backend.set_state(DoorState.Closed): + self.state = DoorState.Closed + self.run_hook('post_lock') + return DoorlockResponse.Success + + return DoorlockResponse.BackendError def request(self, state): old_state = self.state diff --git a/pydoorlock/DoorlockBackend.py b/pydoorlock/DoorlockBackend.py new file mode 100644 index 0000000..9de1975 --- /dev/null +++ b/pydoorlock/DoorlockBackend.py @@ -0,0 +1,17 @@ +from abc import ABC, abstractmethod + +class DoorlockBackend(ABC): + def __init__(self): + self.callbacks = list() + self.current_state = None + + def update_state(self): + self.state_change_callback(self.current_state) + + @abstractmethod + def set_state(self, state): + self.current_state = state + + @abstractmethod + def get_state(self, state): + return self.current_state \ No newline at end of file diff --git a/pydoorlock/SimulationBackend.py b/pydoorlock/SimulationBackend.py new file mode 100644 index 0000000..80ecd99 --- /dev/null +++ b/pydoorlock/SimulationBackend.py @@ -0,0 +1,5 @@ +from .DoorlockBackend import DoorlockBackend + +class SimulationBackend(DoorlockBackend): + def __init__(self, handler): + super.__init__(handler)