Mercurial > code > home > repos > homeauto
annotate service/tomatoWifi/tomatoWifi.py @ 51:d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
Ignore-this: dc012ad1d539d3a1c37c779e63c8cf4b
author | drewp@bigasterisk.com |
---|---|
date | Mon, 31 Dec 2012 15:01:04 -0800 |
parents | 56eeda98cac5 |
children | 875a37be1228 |
rev | line source |
---|---|
0 | 1 #!/usr/bin/python |
2 """ | |
3 scrape the tomato router status pages to see who's connected to the | |
4 wifi access points. Includes leases that aren't currently connected. | |
5 | |
6 Returns: | |
7 json listing (for magma page) | |
8 rdf graph (for reasoning) | |
9 activity stream, when we start saving history | |
10 | |
11 Todo: this should be the one polling and writing to mongo, not entrancemusic | |
12 """ | |
13 from __future__ import division | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
14 import sys, cyclone.web, json, traceback, time, pystache, datetime, logging |
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
15 from cyclone.httpclient import fetch |
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
16 sys.path.append("/home/drewp/projects/photo/lib/python2.7/site-packages") |
0 | 17 from dateutil import tz |
18 from twisted.internet import reactor, task | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
19 from twisted.internet.defer import inlineCallbacks, returnValue |
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
20 |
0 | 21 |
22 from pymongo import Connection, DESCENDING | |
23 from rdflib import Namespace, Literal, URIRef | |
24 sys.path.append("/my/site/magma") | |
25 from stategraph import StateGraph | |
26 from wifi import Wifi | |
27 | |
28 sys.path.append("/my/proj/homeauto/lib") | |
29 from cycloneerr import PrettyErrorHandler | |
1 | 30 from logsetup import log |
0 | 31 |
36 | 32 import rdflib |
33 from rdflib import plugin | |
34 plugin.register( | |
35 "sparql", rdflib.query.Processor, | |
36 "rdfextras.sparql.processor", "Processor") | |
37 plugin.register( | |
38 "sparql", rdflib.query.Result, | |
39 "rdfextras.sparql.query", "SPARQLQueryResult") | |
40 | |
0 | 41 DEV = Namespace("http://projects.bigasterisk.com/device/") |
42 ROOM = Namespace("http://projects.bigasterisk.com/room/") | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
43 reasoning = "http://bang:9071/" |
1 | 44 |
0 | 45 class Index(PrettyErrorHandler, cyclone.web.RequestHandler): |
46 def get(self): | |
47 | |
48 age = time.time() - self.settings.poller.lastPollTime | |
49 if age > 10: | |
50 raise ValueError("poll data is stale. age=%s" % age) | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
51 |
0 | 52 self.write("this is wifiusage. needs index page that embeds the table") |
53 | |
54 class Table(PrettyErrorHandler, cyclone.web.RequestHandler): | |
55 def get(self): | |
56 def rowDict(addr): | |
57 addr['cls'] = "signal" if addr.get('signal') else "nosignal" | |
58 if 'lease' in addr: | |
59 addr['lease'] = addr['lease'].replace("0 days, ", "") | |
60 return addr | |
61 | |
62 self.set_header("Content-Type", "application/xhtml+xml") | |
63 self.write(pystache.render( | |
64 open("table.mustache").read(), | |
65 dict( | |
66 rows=sorted(map(rowDict, self.settings.poller.lastAddrs), | |
67 key=lambda a: (a.get('router'), | |
68 a.get('name'), | |
69 a.get('mac')))))) | |
70 | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
71 |
0 | 72 class Json(PrettyErrorHandler, cyclone.web.RequestHandler): |
73 def get(self): | |
74 self.set_header("Content-Type", "application/json") | |
75 age = time.time() - self.settings.poller.lastPollTime | |
76 if age > 10: | |
77 raise ValueError("poll data is stale. age=%s" % age) | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
78 self.write(json.dumps({"wifi" : self.settings.poller.lastAddrs, |
0 | 79 "dataAge" : age})) |
80 | |
81 class GraphHandler(PrettyErrorHandler, cyclone.web.RequestHandler): | |
82 def get(self): | |
83 g = StateGraph(ctx=DEV['wifi']) | |
84 | |
85 # someday i may also record specific AP and their strength, | |
86 # for positioning. But many users just want to know that the | |
87 # device is connected to some bigasterisk AP. | |
88 aps = URIRef("http://bigasterisk.com/wifiAccessPoints") | |
89 age = time.time() - self.settings.poller.lastPollTime | |
90 if age > 10: | |
91 raise ValueError("poll data is stale. age=%s" % age) | |
92 | |
93 for dev in self.settings.poller.lastAddrs: | |
94 if not dev.get('signal'): | |
95 continue | |
96 uri = URIRef("http://bigasterisk.com/wifiDevice/%s" % dev['mac']) | |
97 g.add((uri, ROOM['macAddress'], Literal(dev['mac']))) | |
98 g.add((uri, ROOM['connected'], aps)) | |
99 if 'rawName' in dev: | |
100 g.add((uri, ROOM['wifiNetworkName'], Literal(dev['rawName']))) | |
101 g.add((uri, ROOM['deviceName'], Literal(dev['name']))) | |
102 g.add((uri, ROOM['signalStrength'], Literal(dev['signal']))) | |
103 | |
104 self.set_header('Content-type', 'application/x-trig') | |
105 self.write(g.asTrig()) | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
106 |
0 | 107 class Application(cyclone.web.Application): |
108 def __init__(self, wifi, poller): | |
109 handlers = [ | |
110 (r"/", Index), | |
111 (r'/json', Json), | |
112 (r'/graph', GraphHandler), | |
113 (r'/table', Table), | |
114 #(r'/activity', Activity), | |
115 ] | |
116 settings = { | |
117 'wifi' : wifi, | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
118 'poller' : poller, |
0 | 119 'mongo' : Connection('bang', 27017, |
120 tz_aware=True)['house']['sensor'] | |
121 } | |
122 cyclone.web.Application.__init__(self, handlers, **settings) | |
123 | |
124 class Poller(object): | |
125 def __init__(self, wifi, mongo): | |
126 self.wifi = wifi | |
127 self.mongo = mongo | |
128 self.lastAddrs = [] | |
129 self.lastWithSignal = [] | |
130 self.lastPollTime = 0 | |
131 | |
132 def assertCurrent(self): | |
133 dt = time.time() - self.lastPollTime | |
134 assert dt < 10, "last poll was %s sec ago" % dt | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
135 |
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
136 @inlineCallbacks |
0 | 137 def poll(self): |
138 try: | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
139 newAddrs = yield self.wifi.getPresentMacAddrs() |
0 | 140 |
141 newWithSignal = [a for a in newAddrs if a.get('signal')] | |
142 | |
143 actions = self.computeActions(newWithSignal) | |
144 for action in actions: | |
1 | 145 log.info("action: %s", action) |
0 | 146 action['created'] = datetime.datetime.now(tz.gettz('UTC')) |
147 mongo.save(action) | |
148 try: | |
149 self.doEntranceMusic(action) | |
150 except Exception, e: | |
1 | 151 log.error("entrancemusic error: %r", e) |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
152 |
0 | 153 self.lastWithSignal = newWithSignal |
50 | 154 if actions: # this doesn't currently include signal strength changes |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
155 fetch(reasoning + "immediateUpdate", |
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
156 headers={'user-agent': 'tomatoWifi'}).addErrback(log.warn) |
0 | 157 self.lastAddrs = newAddrs |
158 self.lastPollTime = time.time() | |
159 except Exception, e: | |
1 | 160 log.error("poll error: %s\n%s", e, traceback.format_exc()) |
0 | 161 |
162 def computeActions(self, newWithSignal): | |
163 def removeVolatile(a): | |
164 ret = dict((k,v) for k,v in a.items() if k in ['name', 'mac']) | |
165 ret['signal'] = bool(a.get('signal')) | |
166 return ret | |
167 | |
168 def find(a, others): | |
169 a = removeVolatile(a) | |
170 return any(a == removeVolatile(o) for o in others) | |
171 | |
172 actions = [] | |
173 | |
174 def makeAction(addr, act): | |
175 return dict(sensor="wifi", | |
176 address=addr.get('mac'), | |
177 name=addr.get('name'), | |
178 networkName=addr.get('rawName'), | |
179 action=act) | |
180 | |
181 for addr in newWithSignal: | |
182 if not find(addr, self.lastWithSignal): | |
183 # the point of all the removeVolatile stuff is so | |
184 # I have the complete addr object here, although | |
185 # it is currently mostly thrown out by makeAction | |
186 actions.append(makeAction(addr, 'arrive')) | |
187 | |
188 for addr in self.lastWithSignal: | |
189 if not find(addr, newWithSignal): | |
190 actions.append(makeAction(addr, 'leave')) | |
191 | |
192 return actions | |
193 | |
194 | |
1 | 195 # these need to move out to their own service |
0 | 196 def doEntranceMusic(self, action): |
1 | 197 import restkit, jsonlib |
0 | 198 dt = self.deltaSinceLastArrive(action['name']) |
1 | 199 log.debug("dt=%s", dt) |
0 | 200 if dt > datetime.timedelta(hours=1): |
201 hub = restkit.Resource( | |
202 # PSHB not working yet; "http://bang:9030/" | |
203 "http://slash:9049/" | |
204 ) | |
205 action = action.copy() | |
206 del action['created'] | |
1 | 207 del action['_id'] |
208 log.info("post to %s", hub) | |
0 | 209 hub.post("visitorNet", payload=jsonlib.dumps(action)) |
210 | |
211 def deltaSinceLastArrive(self, name): | |
212 results = list(self.mongo.find({'name' : name}).sort('created', | |
213 DESCENDING).limit(1)) | |
214 if not results: | |
215 return datetime.timedelta.max | |
216 now = datetime.datetime.now(tz.gettz('UTC')) | |
217 last = results[0]['created'].replace(tzinfo=tz.gettz('UTC')) | |
218 return now - last | |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
219 |
0 | 220 |
221 if __name__ == '__main__': | |
222 config = { | |
223 'servePort' : 9070, | |
224 'pollFrequency' : 1/5, | |
225 } | |
1 | 226 from twisted.python import log as twlog |
0 | 227 #log.startLogging(sys.stdout) |
1 | 228 #log.setLevel(10) |
51
d2842eedd56d
rewrite tomatowifi from restkit to cyclone httpclient
drewp@bigasterisk.com
parents:
50
diff
changeset
|
229 log.setLevel(logging.DEBUG) |
0 | 230 |
231 mongo = Connection('bang', 27017)['visitor']['visitor'] | |
232 | |
233 wifi = Wifi() | |
234 poller = Poller(wifi, mongo) | |
235 task.LoopingCall(poller.poll).start(1/config['pollFrequency']) | |
236 | |
237 reactor.listenTCP(config['servePort'], Application(wifi, poller)) | |
238 reactor.run() |