1 from libtuer import ThreadFunction, logger, fire_and_forget, fire_and_forget_cmd
2 from actor import Actor
3 import os, random, time, threading
5 # logger.{debug,info,warning,error,critical}
9 soundfiles = os.listdir(SOUNDS_DIRECTORY+what)
10 except FileNotFoundError:
11 logger.error("StateMachine: Unable to list sound files in %s" % (SOUNDS_DIRECTORY+what))
13 soundfile = SOUNDS_DIRECTORY + what + '/' + random.choice(soundfiles)
14 fire_and_forget_cmd ([SOUNDS_PLAYER,soundfile], "StateMachine: ")
16 # convert an absolute nervlist to a relative one
17 def nervlist_abs2rel(nervlist_abs):
20 for (t, f) in nervlist_abs:
22 nervlist_rel.append((t-last_t, f))
27 HELLO_PROBABILITY = 0.2
29 # StateUnlocking constants
30 OPEN_REPEAT_TIMEOUT = 7
31 OPEN_REPEAT_NUMBER = 3
33 # StateLocking constants
34 CLOSE_REPEAT_TIMEOUT = 7
35 CLOSE_REPEAT_NUMBER = 3
37 # StateFallback constants
38 FALLBACK_LEAVE_DELAY_LOCK = 5 # seconds
40 # StateAboutToOpen constants
41 SWITCH_PRAISE_PROBABILITY = 0.5
42 ABOUTOPEN_NERVLIST = nervlist_abs2rel([(10, lambda:play_sound("flipswitch")),\
43 (20, lambda:play_sound("flipswitch")), (30, lambda:play_sound("flipswitch")), (30, lambda:logger.error("Space open but switch not flipped for 30 seconds")),\
44 (40, lambda:play_sound("flipswitch")), (50, lambda:play_sound("flipswitch")), (60, lambda:play_sound("mail_sent")),
45 (60, lambda:logger.critical("Space open but switch not flipped for 60 seconds")), (120, lambda:play_sound("mail_sent")),
46 (10*60, lambda:logger.critical("Space open but switch not flipped for 10 minutes")),
47 (60*60, lambda:logger.critical("Space open but switch not flipped for one hour"))])
49 # Timeout we wait after the switch was switched to "Closed", until we assume nobody will open the door and we just lock it
50 # ALso the time we wait after the door was opend, till we assume something went wrong and start nerving
53 # play_sound constants
54 SOUNDS_DIRECTORY = "/opt/tuer/sounds/"
55 SOUNDS_PLAYER = "/usr/bin/mplayer"
59 # A little utility class used to run the nervlists. A nervlist is a list of (n, f) tuples where f() is run after n seconds.
60 # If f returns something, that's also returned by nerv.
61 def __init__(self, nervlist):
62 self.nervlist = list(nervlist)
63 self.last_event_time = time.time()
66 if len(self.nervlist):
67 (wait_time, f) = self.nervlist[0]
69 time_gone = now-self.last_event_time
70 # check if the first element is to be triggered
71 if time_gone >= wait_time:
72 self.nervlist = self.nervlist[1:] # "pop" the first element, but do not modify original list
73 self.last_event_time = now
78 # commands you can send
86 def __init__(self, state_machine, nervlist = None):
87 self.state_machine = state_machine
88 self._nerver = None if nervlist is None else Nerver(nervlist)
89 def handle_pins_event(self):
90 pass # one needn't implement this
91 def handle_buzz_event(self,arg): # this shouldn't be overwritten
92 self.actor().act(Actor.CMD_BUZZ)
93 arg("200 okay: buzz executed")
94 def handle_cmd_unlock_event(self,arg):
96 arg("412 Precondition Failed: The current state (%s) cannot handle the UNLOCK command. Try again later." % self.__class__.__name__)
97 def handle_wakeup_event(self):
98 if self._nerver is not None:
99 return self._nerver.nerv()
103 return self.state_machine.pins
105 return self.state_machine.old_pins
107 return self.state_machine.actor
109 return self.state_machine.api
110 def handle_event(self,ev,arg): # don't override
111 if ev == StateMachine.CMD_PINS:
112 return self.handle_pins_event()
113 elif ev == StateMachine.CMD_BUZZ:
114 return self.handle_buzz_event(arg)
115 elif ev == StateMachine.CMD_UNLOCK:
116 return self.handle_cmd_unlock_event(arg)
117 elif ev == StateMachine.CMD_WAKEUP:
118 return self.handle_wakeup_event()
120 raise Exception("Unknown command number: %d" % ev)
122 class AbstractNonStartState(State):
123 def handle_pins_event(self):
124 if self.pins().door_locked != (not self.pins().space_active):
125 self.actor().act(Actor.CMD_RED_ON)
127 self.actor().act(Actor.CMD_RED_OFF)
128 return super().handle_pins_event()
130 class AbstractLockedState(AbstractNonStartState):
131 '''A state with invariant "The space is locked", switching to StateAboutToOpen when the space becomes unlocked'''
132 def __init__(self, sm, nervlist = None):
133 super().__init__(sm, nervlist)
134 self.actor().act(Actor.CMD_GREEN_OFF)
135 def handle_pins_event(self):
136 if not self.pins().door_locked:
137 logger.info("Door unlocked, space is about to open")
138 return StateMachine.StateAboutToOpen(self.state_machine)
139 return super().handle_pins_event()
141 class AbstractUnlockedState(AbstractNonStartState):
142 '''A state with invariant "The space is unlocked", switching to StateZu when the space becomes locked'''
143 def __init__(self, sm, nervlist = None):
144 super().__init__(sm, nervlist)
145 self.actor().act(Actor.CMD_GREEN_ON)
146 def handle_pins_event(self):
147 if self.pins().door_locked:
148 logger.info("Door locked, closing space")
149 if self.pins().space_active:
150 logger.warning("StateMachine: door manually locked, but space switch is still on - going to StateZu")
151 play_sound("manual_lock")
152 return StateMachine.StateZu(self.state_machine)
153 return super().handle_pins_event()
155 class StateStart(State):
156 def __init__(self, sm, nervlist = None, fallback=False):
157 super().__init__(sm, nervlist)
158 self.fallback = fallback
159 def handle_pins_event(self):
161 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):
163 logger.info("Going to StateFallback because running in fallback mode")
164 return StateMachine.StateFallback(self.state_machine)
166 logger.info("All sensors got a value, switching to a proper state: Space is closed")
167 return StateMachine.StateZu(self.state_machine)
169 logger.info("All sensors got a value, switching to a proper state: Space is (about to) open")
170 return StateMachine.StateAboutToOpen(self.state_machine)
171 return super().handle_pins_event()
173 class StateFallback(State):
174 def __init__(self, sm, nervlist = None):
175 super().__init__(sm, nervlist)
176 self._red_state = False
177 def handle_pins_event(self):
179 # buzz if open and bell rang
180 if pins.space_active and pins.bell_ringing and not self.old_pins().bell_ringing:
181 logger.info("StateFallback: Space switch on and door bell rung => buzzing")
182 self.actor().act(Actor.CMD_BUZZ)
183 # set green LED according to space switch
184 if pins.space_active:
185 self.actor().act(Actor.CMD_GREEN_ON)
187 self.actor().act(Actor.CMD_GREEN_OFF)
188 # primitive leaving procedure if space switch turned off
189 if not pins.space_active and self.old_pins().space_active:
190 def _close_after_time():
191 time.sleep(FALLBACK_LEAVE_DELAY_LOCK)
192 self.actor().act(Actor.CMD_LOCK)
193 fire_and_forget(_close_after_time)
194 # not calling superclass because we want to stay in fallback mode
195 def handle_wakeup_event(self):
198 self.actor().act(Actor.CMD_RED_OFF)
199 self._red_state = False
201 self.actor().act(Actor.CMD_RED_ON)
202 self._red_state = True
203 def handle_cmd_unlock_event(self,arg):
205 arg("298 Fallback Okay: Trying to unlock the door. The System is in fallback mode, success information is not available.")
206 self.actor().act(Actor.CMD_UNLOCK)
208 class StateZu(AbstractLockedState):
209 def handle_cmd_unlock_event(self,callback):
210 return StateMachine.StateUnlocking(self.state_machine, callback)
211 def handle_pins_event(self):
212 if not self.old_pins().space_active and self.pins().space_active: # first thing to check: edge detection
213 logger.info("Space toggled to active while it was closed - unlocking the door")
214 return StateMachine.StateUnlocking(self.state_machine)
215 return super().handle_pins_event()
217 class StateUnlocking(AbstractLockedState):
218 def __init__(self,sm,callback=None):
219 # construct a nervlist
220 nervlist = [(OPEN_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_UNLOCK)) for t in range(OPEN_REPEAT_NUMBER)]
221 nervlist += [(OPEN_REPEAT_TIMEOUT, self.could_not_open)]
222 super().__init__(sm,nervlist)
224 self.actor().act(Actor.CMD_UNLOCK)
225 # enqueue the callback
226 self.handle_cmd_unlock_event(callback)
227 def notify(self, s, lastMsg):
228 for cb in self.callbacks:
231 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))
232 self.notify(s, lastMsg=True)
233 def handle_cmd_unlock_event(self,callback):
234 if callback is not None:
235 callback("202 processing: Trying to unlock the door", lastMsg=False)
236 self.callbacks.append(callback)
237 def could_not_open(self):
238 logger.critical("StateMachine: Couldn't open door after %d tries. Going back to StateZu." % OPEN_REPEAT_NUMBER)
239 return StateMachine.StateZu(self.state_machine)
241 class AbstractStateWhereUnlockingIsRedundant(AbstractUnlockedState):
242 def handle_cmd_unlock_event(self, callback):
243 callback("299 redundant: Space seems to be already open. Still processing your request tough.")
244 logger.info("StateMachine: Received UNLOCK command in %s. This should not be necessary." % self.__class__.__name__)
245 self.actor().act(Actor.CMD_UNLOCK)
247 class StateAboutToOpen(AbstractStateWhereUnlockingIsRedundant):
248 def __init__(self, sm):
249 super().__init__(sm, ABOUTOPEN_NERVLIST)
250 def handle_pins_event(self):
252 if pins.space_active:
253 logger.info("Space activated, opening procedure completed")
254 if not self.old_pins().space_active and random.random() <= SWITCH_PRAISE_PROBABILITY:
255 play_sound("success")
256 return StateMachine.StateAuf(self.state_machine)
257 return super().handle_pins_event()
259 class StateAuf(AbstractStateWhereUnlockingIsRedundant):
260 def __init__(self,sm):
261 nervlist = [(24*60*60, lambda: logger.critical("Space is now open for 24h. Is everything all right?"))]
262 super().__init__(sm, nervlist)
263 self.last_buzzed = None
264 self.api().set_state(True)
265 def handle_pins_event(self):
267 if pins.bell_ringing and not self.old_pins().bell_ringing: # first thing to check: edge detection
268 # someone just pressed the bell
269 logger.info("StateMachine: buzzing because of bell ringing in StateAuf")
270 self.actor().act(Actor.CMD_BUZZ)
271 if not pins.space_active:
272 logger.info("StateMachine: space switch turned off - starting leaving procedure")
273 return StateMachine.StateAboutToLeave(self.state_machine)
274 if not pins.door_closed and self.old_pins().door_closed and random.random() <= HELLO_PROBABILITY:
276 return super().handle_pins_event()
278 self.api().set_state(False)
280 class StateLocking(AbstractUnlockedState):
281 def __init__(self,sm):
282 # construct a nervlist
283 nervlist = [(CLOSE_REPEAT_TIMEOUT, lambda: self.actor().act(Actor.CMD_LOCK)) for t in range(CLOSE_REPEAT_NUMBER)]
284 nervlist += [(CLOSE_REPEAT_TIMEOUT, self.could_not_close)]
285 super().__init__(sm, nervlist)
286 if self.pins().door_closed: # this should always be true, but just to be sure...
287 self.actor().act(Actor.CMD_LOCK)
288 def handle_pins_event(self):
290 if not pins.door_closed:
291 # TODO play a sound? This shouldn't happen, door was opened while we are locking
292 logger.warning("StateMachine: door manually opened during locking")
293 return StateMachine.StateAboutToOpen(self.state_machine)
294 # TODO do anything here if the switch is activated now?
295 return super().handle_pins_event()
296 def handle_cmd_unlock_event(self,callback):
297 callback("409 conflict: The sphinx is currently trying to lock the door. Try again later.")
298 def could_not_close(self):
299 logger.critical("StateMachine: Couldn't close door after %d tries. Going back to StateAboutToOpen." % CLOSE_REPEAT_NUMBER)
300 return StateMachine.StateAboutToOpen(self.state_machine)
302 class StateAboutToLeave(AbstractUnlockedState):
303 def __init__(self, sm):
304 nervlist = [(LEAVE_TIMEOUT, lambda: StateMachine.StateLocking(self.state_machine))]
305 super().__init__(sm, nervlist)
306 def handle_pins_event(self):
307 if not self.pins().door_closed:
308 return StateMachine.StateLeaving(self.state_machine)
309 if self.pins().space_active:
310 logger.info("Space re-activated, cancelling leaving procedure")
311 return StateMachine.StateAuf(self.state_machine)
312 return super().handle_pins_event()
314 class StateLeaving(AbstractUnlockedState):
315 def __init__(self, sm):
316 nervlist = [(LEAVE_TIMEOUT, lambda: StateMachine.StateAboutToOpen(self.state_machine))]
317 super().__init__(sm, nervlist)
318 def handle_pins_event(self):
319 if self.pins().door_closed:
320 logger.info("The space was left, locking the door")
321 return StateMachine.StateLocking(self.state_machine)
322 if self.pins().space_active:
323 logger.info("Space re-activated, cancelling leaving procedure")
324 return StateMachine.StateAuf(self.state_machine)
325 return super().handle_pins_event()
327 def __init__(self, actor, waker, api, fallback = False):
330 self.callback = ThreadFunction(self._callback, name="StateMachine")
331 self.current_state = StateMachine.StateStart(self, fallback=fallback)
334 waker.register(lambda: self.callback(StateMachine.CMD_WAKEUP), 1.0) # wake up every second
335 # initially, the space is closed
341 def _callback(self, cmd, arg=None):
343 if cmd == StateMachine.CMD_PINS:
346 newstate = self.current_state.handle_event(cmd,arg) # returns None or an instance of the new state
347 self.old_pins = self.pins
348 while newstate is not None:
349 assert isinstance(newstate, StateMachine.State), "I should get a state"
350 self.current_state.on_leave()
351 logger.debug("StateMachine: Doing state transition %s -> %s" % (self.current_state.__class__.__name__, newstate.__class__.__name__))
352 self.current_state = newstate
353 newstate = self.current_state.handle_event(StateMachine.CMD_PINS, self.pins)