abs2rel works and more sounds
[saartuer.git] / statemachine.py
index 0fc24bdf08e1c4d9b91c80752cf6c6cc97d5e704..4fccee406117750ee33dc074a647e110a35acd31 100644 (file)
@@ -1,6 +1,6 @@
-from libtuer import ThreadFunction, logger, fire_and_forget
+from libtuer import ThreadFunction, logger, fire_and_forget, fire_and_forget_cmd
 from actor import Actor
 from actor import Actor
-import os, random, time
+import os, random, time, threading
 
 # logger.{debug,info,warning,error,critical}
 
 
 # logger.{debug,info,warning,error,critical}
 
@@ -11,8 +11,20 @@ def play_sound (what):
                logger.error("StateMachine: Unable to list sound files in %s" % (SOUNDS_DIRECTORY+what))
                return
        soundfile = SOUNDS_DIRECTORY + what + '/' + random.choice(soundfiles)
                logger.error("StateMachine: Unable to list sound files in %s" % (SOUNDS_DIRECTORY+what))
                return
        soundfile = SOUNDS_DIRECTORY + what + '/' + random.choice(soundfiles)
-       fire_and_forget ([SOUNDS_PLAYER,soundfile], logger.error, "StateMachine: ")
+       fire_and_forget_cmd ([SOUNDS_PLAYER,soundfile], "StateMachine: ")
 
 
+# convert an absolute nervlist to a relative one
+def nervlist_abs2rel(nervlist_abs):
+       nervlist_rel = []
+       last_t = 0
+       for (t, f) in nervlist_abs:
+               assert t >= last_t
+               nervlist_rel.append((t-last_t, f))
+               last_t = t
+       return nervlist_rel
+
+# StateAuf constants
+HELLO_PROBABILITY = 0.2
 
 # StateUnlocking constants
 OPEN_REPEAT_TIMEOUT = 7
 
 # StateUnlocking constants
 OPEN_REPEAT_TIMEOUT = 7
@@ -22,11 +34,17 @@ OPEN_REPEAT_NUMBER = 3
 CLOSE_REPEAT_TIMEOUT = 7
 CLOSE_REPEAT_NUMBER = 3
 
 CLOSE_REPEAT_TIMEOUT = 7
 CLOSE_REPEAT_NUMBER = 3
 
+# StateFallback constants
+FALLBACK_LEAVE_DELAY_LOCK = 5 # seconds
+
 # StateAboutToOpen constants
 # StateAboutToOpen constants
-ABOUTOPEN_NERVLIST = [(5, lambda : play_sound("flipswitch")), (5, lambda:play_sound("flipswitch")), (0, lambda:logger.warning("Space open but switch not flipped for 10 seconds")),\
-       (10, lambda:play_sound("flipswitch")), (10, lambda:play_sound("flipswitch")), (0, lambda:logger.error("Space open but switch not flipped for 30 seconds")),\
-       (10, lambda:play_sound("flipswitch")), (10, lambda:play_sound("flipswitch")), (6, lambda:play_sound("flipswitch")), (4, lambda:logger.critical("Space open but switch not flipped for 60 seconds")),
-       (59*60, lambda:logger.critical("Space open but switch not flipped for one hour"))]
+SWITCH_PRAISE_PROBABILITY = 0.5
+ABOUTOPEN_NERVLIST = nervlist_abs2rel([(5, lambda : play_sound("flipswitch")), (10, lambda:play_sound("flipswitch")),\
+       (20, lambda:play_sound("flipswitch")), (30, lambda:play_sound("flipswitch")), (30, lambda:logger.error("Space open but switch not flipped for 30 seconds")),\
+       (40, lambda:play_sound("flipswitch")), (50, lambda:play_sound("flipswitch")), (60, lambda:play_sound("mail_sent")),
+       (60, lambda:logger.critical("Space open but switch not flipped for 60 seconds")), (120, lambda:play_sound("mail_sent")),
+       (10*60, lambda:logger.critical("Space open but switch not flipped for 10 minutes")),
+       (60*60, lambda:logger.critical("Space open but switch not flipped for one hour"))])
 
 # Timeout we wait after the switch was switched to "Closed", until we assume nobody will open the door and we just lock it
 # ALso the time we wait after the door was opend, till we assume something went wrong and start nerving
 
 # Timeout we wait after the switch was switched to "Closed", until we assume nobody will open the door and we just lock it
 # ALso the time we wait after the door was opend, till we assume something went wrong and start nerving
@@ -87,6 +105,8 @@ class StateMachine():
                        return self.state_machine.old_pins
                def actor(self):
                        return self.state_machine.actor
                        return self.state_machine.old_pins
                def actor(self):
                        return self.state_machine.actor
+               def api(self):
+                       return self.state_machine.api
                def handle_event(self,ev,arg): # don't override
                        if ev == StateMachine.CMD_PINS:
                                return self.handle_pins_event()
                def handle_event(self,ev,arg): # don't override
                        if ev == StateMachine.CMD_PINS:
                                return self.handle_pins_event()
@@ -99,38 +119,100 @@ class StateMachine():
                        else:
                                raise Exception("Unknown command number: %d" % ev)
        
                        else:
                                raise Exception("Unknown command number: %d" % ev)
        
-       class AbstractLockedState(State):
+       class AbstractNonStartState(State):
+               def handle_pins_event(self):
+                       if self.pins().door_locked != (not self.pins().space_active):
+                               self.actor().act(Actor.CMD_RED_ON)
+                       else:
+                               self.actor().act(Actor.CMD_RED_OFF)
+                       return super().handle_pins_event()
+       
+       class AbstractLockedState(AbstractNonStartState):
                '''A state with invariant "The space is locked", switching to StateAboutToOpen when the space becomes unlocked'''
                '''A state with invariant "The space is locked", switching to StateAboutToOpen when the space becomes unlocked'''
+               def __init__(self, sm, nervlist = None):
+                       super().__init__(sm, nervlist)
+                       self.actor().act(Actor.CMD_GREEN_OFF)
                def handle_pins_event(self):
                        if not self.pins().door_locked:
                def handle_pins_event(self):
                        if not self.pins().door_locked:
-                               return StateAboutToOpen(self.state_machine)
-                       return super().handle_pins_event
+                               logger.info("Door unlocked, space is about to open")
+                               return StateMachine.StateAboutToOpen(self.state_machine)
+                       return super().handle_pins_event()
        
        
-       class AbstractUnlockedState(State):
+       class AbstractUnlockedState(AbstractNonStartState):
                '''A state with invariant "The space is unlocked", switching to StateZu when the space becomes locked'''
                '''A state with invariant "The space is unlocked", switching to StateZu when the space becomes locked'''
+               def __init__(self, sm, nervlist = None):
+                       super().__init__(sm, nervlist)
+                       self.actor().act(Actor.CMD_GREEN_ON)
                def handle_pins_event(self):
                        if self.pins().door_locked:
                def handle_pins_event(self):
                        if self.pins().door_locked:
+                               logger.info("Door locked, closing space")
                                if self.pins().space_active:
                                if self.pins().space_active:
-                                       # FIXME the same state can be reached by first locking the door, and then activating the space. What to do then?
-                                       logger.info("StateMachine: door manually locked, but space switch is still on - going to StateZu")
+                                       logger.warning("StateMachine: door manually locked, but space switch is still on - going to StateZu")
                                        play_sound("manual_lock")
                                        play_sound("manual_lock")
-                               return StateZu(self.state_machine)
-                       return super().handle_pins_event
+                               return StateMachine.StateZu(self.state_machine)
+                       return super().handle_pins_event()
        
        class StateStart(State):
        
        class StateStart(State):
+               def __init__(self, sm, nervlist = None, fallback=False):
+                       super().__init__(sm, nervlist)
+                       self.fallback = fallback
                def handle_pins_event(self):
                        pins = self.pins()
                        if not (pins.door_locked is None or pins.door_closed is None or pins.space_active is None or pins.bell_ringing is None):
                def handle_pins_event(self):
                        pins = self.pins()
                        if not (pins.door_locked is None or pins.door_closed is None or pins.space_active is None or pins.bell_ringing is None):
-                               # All sensors got a value, switch to a proper state
+                               if self.fallback:
+                                       logger.info("Going to StateFallback because running in fallback mode")
+                                       return StateMachine.StateFallback(self.state_machine)
                                if pins.door_locked:
                                if pins.door_locked:
+                                       logger.info("All sensors got a value, switching to a proper state: Space is closed")
                                        return StateMachine.StateZu(self.state_machine)
                                else:
                                        return StateMachine.StateZu(self.state_machine)
                                else:
+                                       logger.info("All sensors got a value, switching to a proper state: Space is (about to) open")
                                        return StateMachine.StateAboutToOpen(self.state_machine)
                        return super().handle_pins_event()
        
                                        return StateMachine.StateAboutToOpen(self.state_machine)
                        return super().handle_pins_event()
        
+       class StateFallback(State):
+               def __init__(self, sm, nervlist = None):
+                       super().__init__(sm, nervlist)
+                       self._red_state = False
+               def handle_pins_event(self):
+                       pins = self.pins()
+                       # buzz if open and bell rang
+                       if pins.space_active and pins.bell_ringing and not self.old_pins().bell_ringing:
+                               logger.info("StateFallback: Space switch on and door bell rung => buzzing")
+                               self.actor().act(Actor.CMD_BUZZ)
+                       # set green LED according to space switch
+                       if pins.space_active:
+                               self.actor().act(Actor.CMD_GREEN_ON)
+                       else:
+                               self.actor().act(Actor.CMD_GREEN_OFF)
+                       # primitive leaving procedure if space switch turned off
+                       if not pins.space_active and self.old_pins().space_active:
+                               def _close_after_time():
+                                       time.sleep(FALLBACK_LEAVE_DELAY_LOCK)
+                                       self.actor().act(Actor.CMD_LOCK)
+                               fire_and_forget(_close_after_time)
+                       # not calling superclass because we want to stay in fallback mode
+               def handle_wakeup_event(self):
+                       # blink red LED
+                       if self._red_state:
+                               self.actor().act(Actor.CMD_RED_OFF)
+                               self._red_state = False
+                       else:
+                               self.actor().act(Actor.CMD_RED_ON)
+                               self._red_state = True
+               def handle_cmd_unlock_event(self,arg):
+                       if arg is not None:
+                               arg("298 Fallback Okay: Trying to unlock the door. The System is in fallback mode, success information is not available.")
+                       self.actor().act(Actor.CMD_UNLOCK)
+       
        class StateZu(AbstractLockedState):
                def handle_cmd_unlock_event(self,callback):
                        return StateMachine.StateUnlocking(self.state_machine, callback)
        class StateZu(AbstractLockedState):
                def handle_cmd_unlock_event(self,callback):
                        return StateMachine.StateUnlocking(self.state_machine, callback)
+               def handle_pins_event(self):
+                       if not self.old_pins().space_active and self.pins().space_active: # first thing to check: edge detection
+                               logger.info("Space toggled to active while it was closed - unlocking the door")
+                               return StateMachine.StateUnlocking(self.state_machine)
+                       return super().handle_pins_event()
        
        class StateUnlocking(AbstractLockedState):
                def __init__(self,sm,callback=None):
        
        class StateUnlocking(AbstractLockedState):
                def __init__(self,sm,callback=None):
@@ -138,19 +220,20 @@ class StateMachine():
                        nervlist = [(OPEN_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_UNLOCK)) for t in range(OPEN_REPEAT_NUMBER)]
                        nervlist += [(OPEN_REPEAT_TIMEOUT, self.could_not_open)]
                        super().__init__(sm,nervlist)
                        nervlist = [(OPEN_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_UNLOCK)) for t in range(OPEN_REPEAT_NUMBER)]
                        nervlist += [(OPEN_REPEAT_TIMEOUT, self.could_not_open)]
                        super().__init__(sm,nervlist)
-                       self.callbacks=[callback]
-                       # TODO: can we send "202 processing: Trying to unlock the door" here? Are the callbacks multi-use?
+                       self.callbacks = []
                        self.actor().act(Actor.CMD_UNLOCK)
                        self.actor().act(Actor.CMD_UNLOCK)
-               def notify(self, did_it_work):
-                       s = "200 okay: door unlocked" if did_it_work else ("500 internal server error: Couldn't unlock door with %d tries à %f seconds" % (OPEN_REPEAT_NUMBER,OPEN_REPEAT_TIMEOUT))
+                       # enqueue the callback
+                       self.handle_cmd_unlock_event(callback)
+               def notify(self, s, lastMsg):
                        for cb in self.callbacks:
                        for cb in self.callbacks:
-                               if cb is not None:
-                                       cb(s)
+                               cb(s, lastMsg)
                def on_leave(self):
                def on_leave(self):
-                       self.notify(not self.pins().door_locked)
+                       s = "200 okay: door unlocked" if not self.pins().door_locked else ("500 internal server error: Couldn't unlock door with %d tries à %f seconds" % (OPEN_REPEAT_NUMBER,OPEN_REPEAT_TIMEOUT))
+                       self.notify(s, lastMsg=True)
                def handle_cmd_unlock_event(self,callback):
                def handle_cmd_unlock_event(self,callback):
-                       # TODO: 202 notification also here if possible
-                       self.callbacks.append(callback)
+                       if callback is not None:
+                               callback("202 processing: Trying to unlock the door", lastMsg=False)
+                               self.callbacks.append(callback)
                def could_not_open(self):
                        logger.critical("StateMachine: Couldn't open door after %d tries. Going back to StateZu." % OPEN_REPEAT_NUMBER)
                        return StateMachine.StateZu(self.state_machine)
                def could_not_open(self):
                        logger.critical("StateMachine: Couldn't open door after %d tries. Going back to StateZu." % OPEN_REPEAT_NUMBER)
                        return StateMachine.StateZu(self.state_machine)
@@ -167,6 +250,9 @@ class StateMachine():
                def handle_pins_event(self):
                        pins = self.pins()
                        if pins.space_active:
                def handle_pins_event(self):
                        pins = self.pins()
                        if pins.space_active:
+                               logger.info("Space activated, opening procedure completed")
+                               if not self.old_pins().space_active and random.random() <= SWITCH_PRAISE_PROBABILITY:
+                                       play_sound("success")
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
@@ -175,16 +261,21 @@ class StateMachine():
                        nervlist = [(24*60*60, lambda: logger.critical("Space is now open for 24h. Is everything all right?"))]
                        super().__init__(sm, nervlist)
                        self.last_buzzed = None
                        nervlist = [(24*60*60, lambda: logger.critical("Space is now open for 24h. Is everything all right?"))]
                        super().__init__(sm, nervlist)
                        self.last_buzzed = None
+                       self.api().set_state(True)
                def handle_pins_event(self):
                        pins = self.pins()
                def handle_pins_event(self):
                        pins = self.pins()
-                       if pins.bell_ringing and not self.old_pins().bell_ringing:
+                       if pins.bell_ringing and not self.old_pins().bell_ringing: # first thing to check: edge detection
                                # someone just pressed the bell
                                logger.info("StateMachine: buzzing because of bell ringing in StateAuf")
                                self.actor().act(Actor.CMD_BUZZ)
                        if not pins.space_active:
                                logger.info("StateMachine: space switch turned off - starting leaving procedure")
                                return StateMachine.StateAboutToLeave(self.state_machine)
                                # someone just pressed the bell
                                logger.info("StateMachine: buzzing because of bell ringing in StateAuf")
                                self.actor().act(Actor.CMD_BUZZ)
                        if not pins.space_active:
                                logger.info("StateMachine: space switch turned off - starting leaving procedure")
                                return StateMachine.StateAboutToLeave(self.state_machine)
+                       if not pins.door_closed and self.old_pins().door_closed and random.random() <= HELLO_PROBABILITY:
+                               play_sound("hello")
                        return super().handle_pins_event()
                        return super().handle_pins_event()
+               def on_leave(self):
+                       self.api().set_state(False)
        
        class StateLocking(AbstractUnlockedState):
                def __init__(self,sm):
        
        class StateLocking(AbstractUnlockedState):
                def __init__(self,sm):
@@ -216,6 +307,7 @@ class StateMachine():
                        if not self.pins().door_closed:
                                return StateMachine.StateLeaving(self.state_machine)
                        if self.pins().space_active:
                        if not self.pins().door_closed:
                                return StateMachine.StateLeaving(self.state_machine)
                        if self.pins().space_active:
+                               logger.info("Space re-activated, cancelling leaving procedure")
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
@@ -225,17 +317,23 @@ class StateMachine():
                        super().__init__(sm, nervlist)
                def handle_pins_event(self):
                        if self.pins().door_closed:
                        super().__init__(sm, nervlist)
                def handle_pins_event(self):
                        if self.pins().door_closed:
+                               logger.info("The space was left, locking the door")
                                return StateMachine.StateLocking(self.state_machine)
                        if self.pins().space_active:
                                return StateMachine.StateLocking(self.state_machine)
                        if self.pins().space_active:
+                               logger.info("Space re-activated, cancelling leaving procedure")
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
                                return StateMachine.StateAuf(self.state_machine)
                        return super().handle_pins_event()
        
-       def __init__(self, actor):
+       def __init__(self, actor, waker, api, fallback = False):
                self.actor = actor
                self.actor = actor
+               self.api = api
                self.callback = ThreadFunction(self._callback, name="StateMachine")
                self.callback = ThreadFunction(self._callback, name="StateMachine")
-               self.current_state = StateMachine.StateStart(self)
+               self.current_state = StateMachine.StateStart(self, fallback=fallback)
                self.pins = None
                self.old_pins = None
                self.pins = None
                self.old_pins = None
+               waker.register(lambda: self.callback(StateMachine.CMD_WAKEUP), 1.0) # wake up every second
+               # initially, the space is closed
+               api.set_state(False)
        
        def stop (self):
                self.callback.stop()
        
        def stop (self):
                self.callback.stop()
@@ -248,6 +346,7 @@ class StateMachine():
                newstate = self.current_state.handle_event(cmd,arg) # returns None or an instance of the new state
                self.old_pins = self.pins
                while newstate is not None:
                newstate = self.current_state.handle_event(cmd,arg) # returns None or an instance of the new state
                self.old_pins = self.pins
                while newstate is not None:
+                       assert isinstance(newstate, StateMachine.State), "I should get a state"
                        self.current_state.on_leave()
                        logger.debug("StateMachine: Doing state transition %s -> %s" % (self.current_state.__class__.__name__, newstate.__class__.__name__))
                        self.current_state = newstate
                        self.current_state.on_leave()
                        logger.debug("StateMachine: Doing state transition %s -> %s" % (self.current_state.__class__.__name__, newstate.__class__.__name__))
                        self.current_state = newstate