view service/frontDoorLock/front_door_lock.py @ 1229:02e4b84821d5

talk to store graph, second button for holding unlocked, etc Ignore-this: c2ae7d756e743c26e5e01d99772899bd darcs-hash:a0750d0bbc4dc7c0f65f63f3e7342b35a175141b
author drewp <drewp@bigasterisk.com>
date Thu, 04 Apr 2019 02:16:22 -0700
parents a72e9245cc72
children 756ff1170342
line wrap: on
line source

"""
:frontDoorLock :state :locked/:unlocked
is the true state of the lock, maintained in this process.

put :frontDoorLock :state ?s to this /output to request a change.

reasoning can infer :frontDoorLock :putState ?s to do that put request.
"""
from docopt import docopt
from patchablegraph import PatchableGraph, CycloneGraphHandler, CycloneGraphEventsHandler
from rdflib import Namespace, URIRef, Literal, Graph
from rdflib.parser import StringInputSource
from twisted.internet import reactor, task
import cyclone.web
import logging, time, json
from mqtt_client import MqttClient
from logsetup import log, enableTwistedLog

ROOM = Namespace('http://projects.bigasterisk.com/room/')

ctx = ROOM['frontDoorControl']

def rdfGraphBody(body, headers):
    g = Graph()
    g.parse(StringInputSource(body), format='nt')
    return g

def mqttMessageFromState(state):
    return {
        ROOM['locked']: b'OFF',
        ROOM['unlocked']: b'ON',
        }[state]

def stateFromMqtt(msg):
    return {
        'OFF': ROOM['locked'],
        'ON': ROOM['unlocked'],
    }[msg.decode('ascii')]
    
class OutputPage(cyclone.web.RequestHandler):
    def put(self):
        try:
            user = URIRef(self.request.headers['x-foaf-agent'])
        except KeyError:
            log.warn('request without x-foaf-agent: %s', self.request.headers)
            self.set_status(403, 'need x-foaf-agent')
            return
        arg = self.request.arguments
        if arg.get('s') and arg.get('p'):
            subj = URIRef(arg['s'][-1])
            pred = URIRef(arg['p'][-1])
            obj = URIRef(self.request.body)
            stmt = (subj, pred, obj)
        else:
            g = rdfGraphBody(self.request.body, self.request.headers)
            assert len(g) == 1, len(g)
            stmt = g.triples((None, None, None)).next()
        self._onStatement(user, stmt)
    post = put
    
    def _onStatement(self, user, stmt):
        log.info('put statement %r', stmt)
        if stmt[0:2] == (ROOM['frontDoorLock'], ROOM['state']):
            if stmt[2] == ROOM['unlocked']:
                log.info('unlock for %r', user)
                self.settings.autoLock.onUnlockedStmt()
            if stmt[2] == ROOM['locked']:
                self.settings.autoLock.onLockedStmt()
            self.settings.mqtt.publish("frontdoor/switch/strike/command",
                                       mqttMessageFromState(stmt[2]))
            return
        log.warn("ignoring %s", stmt)


class AutoLock(object):
    def __init__(self, masterGraph, mqtt):
        self.masterGraph = masterGraph
        self.mqtt = mqtt
        self.timeUnlocked = None
        self.autoLockSec = 6 
        self.subj = ROOM['frontDoorLock']
        task.LoopingCall(self.check).start(1)

    def relock(self):
        log.info('autolock is up: requesting lock')
        self.mqtt.publish("frontdoor/switch/strike/command",
                          mqttMessageFromState(ROOM['locked']))

    def reportTimes(self, unlockedFor):
        g = self.masterGraph
        lockIn = self.autoLockSec - int(unlockedFor)
        if lockIn < 0:
            tu = self.timeUnlocked
            log.warn("timeUnlocked %(tu)r, state %(state)s, "
                     "unlockedFor %(unlockedFor)r, lockIn %(lockIn)r", vars())
            lockIn = 0
        g.patchObject(ctx, self.subj, ROOM['unlockedForSec'],
                      Literal(int(unlockedFor)))
        g.patchObject(ctx, self.subj, ROOM['autoLockInSec'],
                      Literal(lockIn))

    def clearReport(self):
        g = self.masterGraph
        g.patchObject(ctx, self.subj, ROOM['unlockedForSec'], None)
        g.patchObject(ctx, self.subj, ROOM['autoLockInSec'], None)
        
    def check(self):
        g = self.masterGraph
        now = time.time()
        state = g._graph.value(self.subj, ROOM['state'])
        if state == ROOM['unlocked']:
            if self.timeUnlocked is None:
                self.timeUnlocked = now
            # *newly* unlocked- this resets on every input stmt
            unlockedFor = now - self.timeUnlocked
            if unlockedFor > self.autoLockSec:
                self.relock()
        else:
            self.timeUnlocked = None
            self.masterGraph.patchObject(ctx, self.subj, ROOM['unlockedForSec'], None)
            self.masterGraph.patchObject(ctx, self.subj, ROOM['autoLockInSec'], None)

            
if __name__ == '__main__':
    arg = docopt("""
    Usage: front_door_lock.py [options]

    -v   Verbose
    """)
    log.setLevel(logging.INFO)
    if arg['-v']:
        enableTwistedLog()
        log.setLevel(logging.DEBUG)

    masterGraph = PatchableGraph()
    mqtt = MqttClient(brokerPort=10010)
    autoclose = AutoLock(masterGraph, mqtt)

    def toGraph(payload):
        log.info('mqtt->graph %r', payload)
        masterGraph.patchObject(ctx, ROOM['frontDoorLock'], ROOM['state'],
                                stateFromMqtt(payload))

    mqtt.subscribe("frontdoor/switch/strike/state").subscribe(on_next=toGraph)
    port = 10011
    reactor.listenTCP(port, cyclone.web.Application(
        [
            (r"/()", cyclone.web.StaticFileHandler,
             {"path": ".", "default_filename": "index.html"}),
            (r"/graph", CycloneGraphHandler, {'masterGraph': masterGraph}),
            (r"/graph/events", CycloneGraphEventsHandler,
             {'masterGraph': masterGraph}),
            (r'/output', OutputPage),
            (r'/bluetoothButton', BluetoothButton),
        ],
        mqtt=mqtt,
        masterGraph=masterGraph,
        autoLock=autoclose,
        debug=arg['-v']),
                      interface='::')
    log.warn('serving on %s', port)

    reactor.run()