115
|
1 """
|
|
2 listener to the POST messages sent by theaterArduino.py when a pin changes.
|
|
3 records interesting events to mongodb, sends further messages.
|
|
4
|
|
5 Will also serve activity stream.
|
|
6 """
|
|
7 import sys, os, datetime, cyclone.web, simplejson, time
|
|
8 from twisted.internet import reactor
|
|
9 from twisted.internet.error import ConnectError
|
|
10 from twisted.internet.defer import inlineCallbacks, returnValue
|
|
11 from twisted.web.client import getPage
|
|
12 from dateutil.tz import tzutc
|
|
13 from pymongo import Connection
|
|
14 from rdflib import Namespace, Literal, Graph
|
|
15 from rdflib.parser import StringInputSource
|
|
16 sys.path.append("/my/site/magma")
|
|
17 from activitystream import ActivityStream
|
|
18 from stategraph import StateGraph
|
|
19
|
|
20 sys.path.append("/my/proj/homeauto/lib")
|
|
21 from cycloneerr import PrettyErrorHandler
|
|
22 from logsetup import log
|
|
23
|
|
24 DEV = Namespace("http://projects.bigasterisk.com/device/")
|
|
25 ROOM = Namespace("http://projects.bigasterisk.com/room/")
|
|
26 zeroTime = datetime.datetime.fromtimestamp(0, tzutc())
|
|
27
|
|
28 class PinChange(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
29 def post(self):
|
|
30 # there should be per-pin debounce settings so we don't log
|
|
31 # all the noise of a transition change
|
|
32
|
|
33 msg = simplejson.loads(self.request.body)
|
|
34 msg['t'] = datetime.datetime.now(tzutc())
|
|
35 msg['name'] = {9: 'downstairsDoorOpen',
|
|
36 10: 'downstairsDoorMotion',
|
|
37 }[msg['pin']]
|
|
38 log.info("pinchange post %r", msg)
|
|
39 self.settings.mongo.insert(msg)
|
|
40
|
|
41 history = self.settings.history
|
|
42 if msg['pin'] == 10:
|
|
43 history['motionHistory'] = (history.get('motionHistory', []) + [(msg['t'], msg['level'])])[-50:]
|
|
44 if msg['level'] == 1:
|
|
45 if history.get('prevMotion', 0) == 0:
|
|
46 history['motionStart'] = msg['t']
|
|
47
|
|
48 history['prevMotion'] = msg['level']
|
|
49
|
|
50
|
|
51 class InputChange(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
52 """
|
|
53 several other inputs post to here to get their events recorded,
|
|
54 too. This file shouldn't be in theaterArduino. See bedroomArduino,
|
|
55 frontDoorArduino, garageArduino.
|
|
56 """
|
|
57 def post(self):
|
|
58 msg = simplejson.loads(self.request.body)
|
|
59 msg['t'] = datetime.datetime.now(tzutc())
|
|
60 log.info(msg)
|
|
61 self.settings.mongo.insert(msg)
|
|
62
|
|
63 # trigger to entrancemusic? rdf graph change PSHB?
|
|
64
|
|
65 class GraphHandler(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
66 """
|
|
67 fetch the pins from drv right now (so we don't have stale data),
|
|
68 and return an rdf graph describing what we know about the world
|
|
69 """
|
|
70 @inlineCallbacks
|
|
71 def get(self):
|
|
72 g = StateGraph(ctx=DEV['houseSensors'])
|
|
73
|
|
74 frontDoorDefer = getPage("http://slash:9080/door", timeout=2) # head start?
|
|
75
|
|
76 doorOpen = int((yield getPage("http://bang:9056/pin/d9", timeout=1)))
|
|
77 g.add((DEV['theaterDoorOpen'], ROOM['state'],
|
|
78 ROOM['open'] if doorOpen else ROOM['closed']))
|
|
79
|
|
80 for s in self.motionStatements(
|
|
81 currentMotion=int((yield getPage("http://bang:9056/pin/d10",
|
|
82 timeout=1)))):
|
|
83 g.add(s)
|
|
84
|
|
85 try:
|
|
86 for s in (yield self.getBedroomStatements()):
|
|
87 g.add(s)
|
|
88 except ConnectError, e:
|
|
89 g.add((ROOM['bedroomStatementFetch'], ROOM['error'],
|
|
90 Literal("getBedroomStatements: %s" % e)))
|
|
91
|
|
92 try:
|
|
93 frontDoor = yield frontDoorDefer
|
|
94 g.add((DEV['frontDoorOpen'], ROOM['state'],
|
|
95 ROOM[frontDoor] if frontDoor in ['open', 'closed'] else
|
|
96 ROOM['error']))
|
|
97 except Exception, e:
|
|
98 g.add((DEV['frontDoorOpen'], ROOM['error'], Literal(str(e))))
|
|
99
|
|
100 self.set_header('Content-type', 'application/x-trig')
|
|
101 self.write(g.asTrig())
|
|
102
|
|
103 def motionStatements(self, currentMotion):
|
|
104 uri = DEV['theaterDoorOutsideMotion']
|
|
105
|
|
106 yield (uri, ROOM['state'], ROOM['motion'] if currentMotion else ROOM['noMotion'])
|
|
107
|
|
108 now = datetime.datetime.now(tzutc())
|
|
109 if currentMotion:
|
|
110 try:
|
|
111 dt = now - self.settings.history['motionStart']
|
|
112 yield (uri, ROOM['motionDurationSec'], Literal(dt.total_seconds()))
|
|
113 if dt > datetime.timedelta(seconds=4):
|
|
114 yield (uri, ROOM['state'], ROOM['sustainedMotion'])
|
|
115 except KeyError:
|
|
116 pass
|
|
117
|
|
118 # this is history without the db, which means the window is
|
|
119 # limited and it could reset any time
|
|
120 if 'motionHistory' in self.settings.history:
|
|
121 yield ((uri, ROOM['history'],
|
|
122 Literal(simplejson.dumps(
|
|
123 [(round((t - now).total_seconds(), ndigits=2), v)
|
|
124 for t,v in self.settings.history['motionHistory']]))))
|
|
125
|
|
126 @inlineCallbacks
|
|
127 def getBedroomStatements(self):
|
|
128 trig = yield getPage("http://bang:9088/graph", timeout=1)
|
|
129 stmts = set()
|
|
130 for line in trig.splitlines():
|
|
131 if "http://projects.bigasterisk.com/device/bedroomMotion" in line:
|
|
132 g = Graph()
|
|
133 g.parse(StringInputSource(line+"\n"), format="nt")
|
|
134 for s in g:
|
|
135 stmts.add(s)
|
|
136 returnValue(stmts)
|
|
137
|
|
138 class Activity(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
139 def get(self):
|
|
140 a = ActivityStream()
|
|
141 self.settings.mongo.ensure_index('t')
|
|
142 remaining = {'downstairsDoorMotion':10, 'downstairsDoorOpen':10,
|
204
|
143 'frontDoorMotion':30, 'frontDoor':50, 'bedroomMotion': 10}
|
115
|
144 recent = {}
|
|
145 toAdd = []
|
|
146 for row in list(self.settings.mongo.find(sort=[('t', -1)],
|
204
|
147 limit=10000)):
|
115
|
148 try:
|
|
149 r = remaining[row['name']]
|
|
150 if r < 1:
|
|
151 continue
|
|
152 remaining[row['name']] = r - 1
|
|
153 except KeyError:
|
|
154 pass
|
|
155
|
|
156 # lots todo
|
|
157 if row['name'] == 'downstairsDoorMotion':
|
|
158 if row['level'] == 0:
|
|
159 continue
|
|
160 kw = dict(
|
|
161 actorUri="http://...",
|
|
162 actorName="downstairs door",
|
|
163 verbUri="...",
|
|
164 verbEnglish="sees",
|
|
165 objectUri="...",
|
|
166 objectName="backyard motion",
|
|
167 objectIcon="/magma/static/backyardMotion.png")
|
|
168 elif row['name'] == 'downstairsDoorOpen':
|
|
169 kw = dict(actorUri="http://bigasterisk.com/foaf/someone",
|
|
170 actorName="someone",
|
|
171 verbUri="op",
|
|
172 verbEnglish="opens" if row['level'] else "closes",
|
|
173 objectUri="...",
|
|
174 objectName="downstairs door",
|
|
175 objectIcon="/magma/static/downstairsDoor.png")
|
|
176 elif row['name'] == 'frontDoor':
|
|
177 kw = dict(actorUri="http://bigasterisk.com/foaf/someone",
|
|
178 actorName="someone",
|
|
179 verbUri="op",
|
|
180 verbEnglish="opens" if row['state']=='open' else "closes",
|
|
181 objectUri="...",
|
|
182 objectName="front door",
|
|
183 objectIcon="/magma/static/frontDoor.png")
|
|
184 elif row['name'] == 'frontDoorMotion':
|
|
185 if row['state'] == False:
|
|
186 continue
|
|
187 if 'frontDoorMotion' in recent:
|
|
188 pass#if row['t'
|
|
189 kw = dict(
|
|
190 actorUri="http://...",
|
|
191 actorName="front door",
|
|
192 verbUri="...",
|
|
193 verbEnglish="sees",
|
|
194 objectUri="...",
|
|
195 objectName="front yard motion",
|
|
196 objectIcon="/magma/static/frontYardMotion.png")
|
|
197 recent['frontDoorMotion'] = kw
|
|
198 elif row['name'] == 'bedroomMotion':
|
|
199 if not row['state']:
|
|
200 continue
|
|
201 kw = dict(
|
|
202 actorUri="http://...",
|
|
203 actorName="bedroom",
|
|
204 verbUri="...",
|
|
205 verbEnglish="sees",
|
|
206 objectUri="...",
|
|
207 objectName="bedroom motion",
|
|
208 objectIcon="/magma/static/bedroomMotion.png")
|
|
209 recent['bedroomMotion'] = kw
|
|
210 else:
|
|
211 raise NotImplementedError(row)
|
|
212
|
|
213 kw.update({'published' : row['t'],
|
|
214 'entryUriComponents' : ('sensor', row['board'])})
|
|
215 toAdd.append(kw)
|
|
216
|
|
217 toAdd.reverse()
|
|
218 for kw in toAdd:
|
|
219 a.addEntry(**kw)
|
|
220
|
|
221 self.set_header("Content-type", "application/atom+xml")
|
|
222 self.write(a.makeAtom())
|
|
223
|
|
224 class Application(cyclone.web.Application):
|
|
225 def __init__(self):
|
|
226 handlers = [
|
|
227 (r'/()', cyclone.web.StaticFileHandler,
|
|
228 {"path" : ".", "default_filename" : "watchpins.html"}),
|
|
229 (r'/pinChange', PinChange),
|
|
230 (r'/inputChange', InputChange),
|
|
231 (r'/activity', Activity),
|
|
232 (r'/graph', GraphHandler),
|
|
233 ]
|
|
234 settings = {
|
|
235 'mongo' : Connection('bang', 27017,
|
|
236 tz_aware=True)['house']['sensor'],
|
|
237 'history' : {
|
|
238 },
|
|
239 }
|
|
240 cyclone.web.Application.__init__(self, handlers, **settings)
|
|
241
|
|
242 if __name__ == '__main__':
|
|
243 #from twisted.python import log as twlog
|
|
244 #twlog.startLogging(sys.stdout)
|
|
245 reactor.listenTCP(9069, Application())
|
|
246 reactor.run()
|