2016-12-30 02:51:56 -05:00
|
|
|
import RPi.GPIO as GPIO
|
|
|
|
import time, logging
|
|
|
|
from threading import Lock
|
|
|
|
from functools import partial
|
|
|
|
from collections import namedtuple
|
|
|
|
|
2017-06-03 17:13:45 -04:00
|
|
|
from auxilary import CountdownTimer, resetUSBDevice
|
2017-06-03 17:34:30 -04:00
|
|
|
from config import stateFile
|
2016-12-30 02:51:56 -05:00
|
|
|
from sensors import setupDoorSensor, setupMotionSensor
|
2017-05-31 00:41:42 -04:00
|
|
|
from gmail import intruderAlert
|
2016-12-30 02:51:56 -05:00
|
|
|
from listeners import KeypadListener, PipeListener
|
|
|
|
from blinkenLights import Blinkenlights
|
|
|
|
from soundLib import SoundLib
|
2017-05-21 14:29:17 -04:00
|
|
|
from webInterface import initWebInterface
|
2017-06-03 01:28:57 -04:00
|
|
|
from stream import Camera, FileDump
|
2016-12-30 02:51:56 -05:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
2017-05-31 00:24:50 -04:00
|
|
|
resetUSBDevice('1-1')
|
|
|
|
|
2016-12-30 02:51:56 -05:00
|
|
|
class SIGNALS:
|
|
|
|
ARM = 1
|
|
|
|
INSTANT_ARM = 2
|
|
|
|
DISARM = 3
|
|
|
|
TIMOUT = 4
|
|
|
|
TRIGGER = 5
|
|
|
|
|
|
|
|
class State:
|
|
|
|
def __init__(self, stateMachine, name, entryCallbacks=[], exitCallbacks=[], blinkLED=True, sound=None):
|
|
|
|
self.stateMachine = stateMachine
|
|
|
|
self.name = name
|
|
|
|
self.entryCallbacks = entryCallbacks
|
|
|
|
self.exitCallbacks = exitCallbacks
|
|
|
|
self.blinkLED = blinkLED
|
|
|
|
|
|
|
|
if not sound and name in stateMachine.soundLib.soundEffects:
|
|
|
|
self.sound = stateMachine.soundLib.soundEffects[name]
|
|
|
|
else:
|
|
|
|
self.sound = sound
|
|
|
|
|
|
|
|
def entry(self):
|
2017-05-30 02:11:15 -04:00
|
|
|
logger.info('entering ' + self.name)
|
2017-05-21 14:29:17 -04:00
|
|
|
if self.sound:
|
|
|
|
self.sound.play()
|
|
|
|
self.stateMachine.LED.blink = self.blinkLED
|
|
|
|
self.stateMachine.keypadListener.resetBuffer()
|
2016-12-30 02:51:56 -05:00
|
|
|
for c in self.entryCallbacks:
|
|
|
|
c()
|
|
|
|
|
|
|
|
def exit(self):
|
2017-05-30 02:11:15 -04:00
|
|
|
logger.info('exiting ' + self.name)
|
2017-05-21 14:29:17 -04:00
|
|
|
if self.sound:
|
|
|
|
self.sound.stop()
|
2016-12-30 02:51:56 -05:00
|
|
|
for c in self.exitCallbacks:
|
|
|
|
c()
|
|
|
|
|
|
|
|
def next(self, signal):
|
|
|
|
t = (self, signal)
|
|
|
|
return self if t not in self.stateMachine.transitionTable else self.stateMachine.transitionTable[t]
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.name
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
return self.name == other
|
|
|
|
|
|
|
|
def __hash__(self):
|
|
|
|
return hash(self.name)
|
|
|
|
|
|
|
|
class StateMachine:
|
2017-05-21 14:29:17 -04:00
|
|
|
def __init__(self):
|
|
|
|
self.soundLib = SoundLib()
|
2016-12-30 02:51:56 -05:00
|
|
|
|
|
|
|
def startTimer(t, sound):
|
|
|
|
self._timer = CountdownTimer(t, partial(self.selectState, SIGNALS.TIMOUT), sound)
|
|
|
|
|
|
|
|
def stopTimer():
|
|
|
|
if self._timer.is_alive():
|
|
|
|
self._timer.stop()
|
|
|
|
self._timer = None
|
|
|
|
|
|
|
|
States = namedtuple('States', ['disarmed', 'disarmedCountdown', 'armed', 'armedCountdown', 'triggered'])
|
|
|
|
|
|
|
|
self.states = States(
|
|
|
|
State(
|
|
|
|
self,
|
|
|
|
name = 'disarmed',
|
|
|
|
blinkLED = False
|
|
|
|
),
|
|
|
|
State(
|
|
|
|
self,
|
|
|
|
name='disarmedCountdown',
|
|
|
|
entryCallbacks = [partial(startTimer, 30, self.soundLib.soundEffects['disarmedCountdown'])],
|
|
|
|
exitCallbacks = [stopTimer]
|
|
|
|
),
|
|
|
|
State(
|
|
|
|
self,
|
|
|
|
name = 'armed'
|
|
|
|
),
|
|
|
|
State(
|
|
|
|
self,
|
|
|
|
name = 'armedCountdown',
|
|
|
|
entryCallbacks = [partial(startTimer, 30, self.soundLib.soundEffects['armedCountdown'])],
|
|
|
|
exitCallbacks = [stopTimer]
|
|
|
|
),
|
|
|
|
State(
|
|
|
|
self,
|
|
|
|
name = 'triggered',
|
|
|
|
entryCallbacks = [intruderAlert]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2017-06-03 17:34:30 -04:00
|
|
|
self.currentState = getattr(self.states, stateFile['state'])
|
2016-12-30 02:51:56 -05:00
|
|
|
|
|
|
|
self.transitionTable = {
|
|
|
|
(self.states.disarmed, SIGNALS.ARM): self.states.disarmedCountdown,
|
|
|
|
(self.states.disarmed, SIGNALS.INSTANT_ARM): self.states.armed,
|
|
|
|
|
|
|
|
(self.states.disarmedCountdown, SIGNALS.DISARM): self.states.disarmed,
|
|
|
|
(self.states.disarmedCountdown, SIGNALS.TIMOUT): self.states.armed,
|
|
|
|
(self.states.disarmedCountdown, SIGNALS.INSTANT_ARM): self.states.armed,
|
|
|
|
|
|
|
|
(self.states.armed, SIGNALS.DISARM): self.states.disarmed,
|
|
|
|
(self.states.armed, SIGNALS.TRIGGER): self.states.armedCountdown,
|
|
|
|
|
|
|
|
(self.states.armedCountdown, SIGNALS.DISARM): self.states.disarmed,
|
|
|
|
(self.states.armedCountdown, SIGNALS.ARM): self.states.armed,
|
|
|
|
(self.states.armedCountdown, SIGNALS.TIMOUT): self.states.triggered,
|
|
|
|
|
|
|
|
(self.states.triggered, SIGNALS.DISARM): self.states.disarmed,
|
|
|
|
(self.states.triggered, SIGNALS.ARM): self.states.armed,
|
|
|
|
}
|
|
|
|
|
|
|
|
self._lock = Lock()
|
|
|
|
|
2017-05-21 14:29:17 -04:00
|
|
|
self.LED = Blinkenlights(17)
|
2016-12-30 02:51:56 -05:00
|
|
|
|
2017-06-03 01:28:57 -04:00
|
|
|
self.camera = Camera()
|
2017-05-30 01:43:09 -04:00
|
|
|
|
2016-12-30 02:51:56 -05:00
|
|
|
def action():
|
2017-05-25 02:41:58 -04:00
|
|
|
if self.currentState == self.states.armed:
|
2016-12-30 02:51:56 -05:00
|
|
|
self.selectState(SIGNALS.TRIGGER)
|
|
|
|
|
2017-06-03 01:28:57 -04:00
|
|
|
self.fileDump = FileDump()
|
2017-06-02 00:15:05 -04:00
|
|
|
sensitiveStates = (self.states.armed, self.states.armedCountdown, self.states.triggered)
|
|
|
|
|
2016-12-30 02:51:56 -05:00
|
|
|
def actionVideo(pin):
|
2017-06-02 00:15:05 -04:00
|
|
|
if self.currentState in sensitiveStates:
|
2016-12-30 02:51:56 -05:00
|
|
|
self.selectState(SIGNALS.TRIGGER)
|
2017-06-03 01:28:57 -04:00
|
|
|
self.fileDump.addInitiator(pin)
|
2017-06-02 00:15:05 -04:00
|
|
|
while GPIO.input(pin) and self.currentState in sensitiveStates:
|
|
|
|
time.sleep(0.1)
|
2017-06-03 01:28:57 -04:00
|
|
|
self.fileDump.removeInitiator(pin)
|
2016-12-30 02:51:56 -05:00
|
|
|
|
2017-05-21 14:29:17 -04:00
|
|
|
setupMotionSensor(5, 'Nate\'s room', action)
|
|
|
|
setupMotionSensor(19, 'front door', action)
|
|
|
|
setupMotionSensor(26, 'Laura\'s room', action)
|
|
|
|
setupMotionSensor(6, 'deck window', partial(actionVideo, 6))
|
|
|
|
setupMotionSensor(13, 'kitchen bar', partial(actionVideo, 13))
|
2016-12-30 02:51:56 -05:00
|
|
|
|
2017-05-21 14:29:17 -04:00
|
|
|
setupDoorSensor(22, action, self.soundLib.soundEffects['door'])
|
2016-12-30 02:51:56 -05:00
|
|
|
|
|
|
|
secretTable = {
|
|
|
|
"dynamoHum": partial(self.selectState, SIGNALS.DISARM),
|
|
|
|
"zombyWoof": partial(self.selectState, SIGNALS.ARM),
|
|
|
|
"imTheSlime": partial(self.selectState, SIGNALS.INSTANT_ARM)
|
|
|
|
}
|
|
|
|
|
2017-05-21 14:29:17 -04:00
|
|
|
def secretCallback(secret, logger):
|
|
|
|
if secret in secretTable:
|
|
|
|
secretTable[secret]()
|
|
|
|
logger.debug('Secret pipe listener received: \"%s\"', secret)
|
|
|
|
elif logger:
|
2017-05-30 02:11:15 -04:00
|
|
|
logger.debug('Secret pipe listener received invalid secret')
|
2017-05-21 14:29:17 -04:00
|
|
|
|
|
|
|
self.secretListener = PipeListener(
|
|
|
|
callback = secretCallback,
|
|
|
|
path = '/tmp/secret'
|
|
|
|
)
|
|
|
|
|
|
|
|
self.keypadListener = KeypadListener(
|
|
|
|
stateMachine = self,
|
|
|
|
callbackDisarm = partial(self.selectState, 'disarm'),
|
|
|
|
callbackArm = partial(self.selectState, 'arm'),
|
|
|
|
soundLib = self.soundLib,
|
|
|
|
passwd = '5918462'
|
|
|
|
)
|
2017-05-30 01:03:07 -04:00
|
|
|
|
2017-05-21 14:29:17 -04:00
|
|
|
initWebInterface(self)
|
|
|
|
|
2016-12-30 02:51:56 -05:00
|
|
|
self.currentState.entry()
|
|
|
|
|
|
|
|
def selectState(self, signal):
|
2017-06-03 17:13:45 -04:00
|
|
|
with self._lock:
|
2016-12-30 02:51:56 -05:00
|
|
|
nextState = self.currentState.next(signal)
|
|
|
|
if nextState != self.currentState:
|
|
|
|
self.currentState.exit()
|
|
|
|
self.currentState = nextState
|
|
|
|
self.currentState.entry()
|
|
|
|
|
2017-06-03 17:34:30 -04:00
|
|
|
stateFile['state'] = self.currentState.name
|
2016-12-30 02:51:56 -05:00
|
|
|
|
|
|
|
logger.info('state changed to %s', self.currentState)
|
|
|
|
|
|
|
|
def __del__(self):
|
2017-05-21 14:29:17 -04:00
|
|
|
if hasattr(self, 'LED'):
|
2017-05-30 02:11:15 -04:00
|
|
|
self.LED.__del__()
|
2016-12-30 02:51:56 -05:00
|
|
|
|
2017-06-03 01:28:57 -04:00
|
|
|
if hasattr(self, 'camera'):
|
|
|
|
self.camera.__del__()
|
|
|
|
|
|
|
|
if hasattr(self, 'fileDump'):
|
|
|
|
self.fileDump.__del__()
|
|
|
|
|
2017-05-21 14:29:17 -04:00
|
|
|
if hasattr(self, 'soundLib'):
|
2016-12-30 02:51:56 -05:00
|
|
|
self.soundLib.__del__()
|
|
|
|
|
2017-05-30 02:11:15 -04:00
|
|
|
if hasattr(self, 'secretListener'):
|
|
|
|
self.secretListener.__del__()
|
2016-12-30 02:51:56 -05:00
|
|
|
|
2017-05-21 14:29:17 -04:00
|
|
|
if hasattr(self, 'keypadListener'):
|
2016-12-30 02:51:56 -05:00
|
|
|
self.keypadListener.__del__()
|