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
|
|
14 import sys, cyclone.web, simplejson, traceback, time, pystache, datetime
|
|
15 from dateutil import tz
|
|
16 from twisted.python import log
|
|
17 from twisted.internet import reactor, task
|
|
18
|
|
19 from pymongo import Connection, DESCENDING
|
|
20 from rdflib import Namespace, Literal, URIRef
|
|
21 sys.path.append("/my/site/magma")
|
|
22 from stategraph import StateGraph
|
|
23 from wifi import Wifi
|
|
24
|
|
25 sys.path.append("/my/proj/homeauto/lib")
|
|
26 from cycloneerr import PrettyErrorHandler
|
|
27
|
|
28 DEV = Namespace("http://projects.bigasterisk.com/device/")
|
|
29 ROOM = Namespace("http://projects.bigasterisk.com/room/")
|
|
30
|
|
31 class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
32 def get(self):
|
|
33
|
|
34 age = time.time() - self.settings.poller.lastPollTime
|
|
35 if age > 10:
|
|
36 raise ValueError("poll data is stale. age=%s" % age)
|
|
37
|
|
38 self.write("this is wifiusage. needs index page that embeds the table")
|
|
39
|
|
40 class Table(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
41 def get(self):
|
|
42 def rowDict(addr):
|
|
43 addr['cls'] = "signal" if addr.get('signal') else "nosignal"
|
|
44 if 'lease' in addr:
|
|
45 addr['lease'] = addr['lease'].replace("0 days, ", "")
|
|
46 return addr
|
|
47
|
|
48 self.set_header("Content-Type", "application/xhtml+xml")
|
|
49 self.write(pystache.render(
|
|
50 open("table.mustache").read(),
|
|
51 dict(
|
|
52 rows=sorted(map(rowDict, self.settings.poller.lastAddrs),
|
|
53 key=lambda a: (a.get('router'),
|
|
54 a.get('name'),
|
|
55 a.get('mac'))))))
|
|
56
|
|
57
|
|
58 class Json(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
59 def get(self):
|
|
60 self.set_header("Content-Type", "application/json")
|
|
61 age = time.time() - self.settings.poller.lastPollTime
|
|
62 if age > 10:
|
|
63 raise ValueError("poll data is stale. age=%s" % age)
|
|
64 self.write(simplejson.dumps({"wifi" : self.settings.poller.lastAddrs,
|
|
65 "dataAge" : age}))
|
|
66
|
|
67 class GraphHandler(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
68 def get(self):
|
|
69 g = StateGraph(ctx=DEV['wifi'])
|
|
70
|
|
71 # someday i may also record specific AP and their strength,
|
|
72 # for positioning. But many users just want to know that the
|
|
73 # device is connected to some bigasterisk AP.
|
|
74 aps = URIRef("http://bigasterisk.com/wifiAccessPoints")
|
|
75 age = time.time() - self.settings.poller.lastPollTime
|
|
76 if age > 10:
|
|
77 raise ValueError("poll data is stale. age=%s" % age)
|
|
78
|
|
79 for dev in self.settings.poller.lastAddrs:
|
|
80 if not dev.get('signal'):
|
|
81 continue
|
|
82 uri = URIRef("http://bigasterisk.com/wifiDevice/%s" % dev['mac'])
|
|
83 g.add((uri, ROOM['macAddress'], Literal(dev['mac'])))
|
|
84 g.add((uri, ROOM['connected'], aps))
|
|
85 if 'rawName' in dev:
|
|
86 g.add((uri, ROOM['wifiNetworkName'], Literal(dev['rawName'])))
|
|
87 g.add((uri, ROOM['deviceName'], Literal(dev['name'])))
|
|
88 g.add((uri, ROOM['signalStrength'], Literal(dev['signal'])))
|
|
89
|
|
90 self.set_header('Content-type', 'application/x-trig')
|
|
91 self.write(g.asTrig())
|
|
92
|
|
93 class Application(cyclone.web.Application):
|
|
94 def __init__(self, wifi, poller):
|
|
95 handlers = [
|
|
96 (r"/", Index),
|
|
97 (r'/json', Json),
|
|
98 (r'/graph', GraphHandler),
|
|
99 (r'/table', Table),
|
|
100 #(r'/activity', Activity),
|
|
101 ]
|
|
102 settings = {
|
|
103 'wifi' : wifi,
|
|
104 'poller' : poller,
|
|
105 'mongo' : Connection('bang', 27017,
|
|
106 tz_aware=True)['house']['sensor']
|
|
107 }
|
|
108 cyclone.web.Application.__init__(self, handlers, **settings)
|
|
109
|
|
110 class Poller(object):
|
|
111 def __init__(self, wifi, mongo):
|
|
112 self.wifi = wifi
|
|
113 self.mongo = mongo
|
|
114 self.lastAddrs = []
|
|
115 self.lastWithSignal = []
|
|
116 self.lastPollTime = 0
|
|
117
|
|
118 def assertCurrent(self):
|
|
119 dt = time.time() - self.lastPollTime
|
|
120 assert dt < 10, "last poll was %s sec ago" % dt
|
|
121
|
|
122 def poll(self):
|
|
123 try:
|
|
124 newAddrs = self.wifi.getPresentMacAddrs()
|
|
125
|
|
126 newWithSignal = [a for a in newAddrs if a.get('signal')]
|
|
127
|
|
128 actions = self.computeActions(newWithSignal)
|
|
129 for action in actions:
|
|
130 action['created'] = datetime.datetime.now(tz.gettz('UTC'))
|
|
131 mongo.save(action)
|
|
132 try:
|
|
133 self.doEntranceMusic(action)
|
|
134 except Exception, e:
|
|
135 print "entrancemusic error", e
|
|
136
|
|
137 self.lastWithSignal = newWithSignal
|
|
138 self.lastAddrs = newAddrs
|
|
139 self.lastPollTime = time.time()
|
|
140 except Exception, e:
|
|
141 print "poll error", e
|
|
142 traceback.print_exc()
|
|
143
|
|
144 def computeActions(self, newWithSignal):
|
|
145 def removeVolatile(a):
|
|
146 ret = dict((k,v) for k,v in a.items() if k in ['name', 'mac'])
|
|
147 ret['signal'] = bool(a.get('signal'))
|
|
148 return ret
|
|
149
|
|
150 def find(a, others):
|
|
151 a = removeVolatile(a)
|
|
152 return any(a == removeVolatile(o) for o in others)
|
|
153
|
|
154 actions = []
|
|
155
|
|
156 def makeAction(addr, act):
|
|
157 return dict(sensor="wifi",
|
|
158 address=addr.get('mac'),
|
|
159 name=addr.get('name'),
|
|
160 networkName=addr.get('rawName'),
|
|
161 action=act)
|
|
162
|
|
163 for addr in newWithSignal:
|
|
164 if not find(addr, self.lastWithSignal):
|
|
165 # the point of all the removeVolatile stuff is so
|
|
166 # I have the complete addr object here, although
|
|
167 # it is currently mostly thrown out by makeAction
|
|
168 actions.append(makeAction(addr, 'arrive'))
|
|
169
|
|
170 for addr in self.lastWithSignal:
|
|
171 if not find(addr, newWithSignal):
|
|
172 actions.append(makeAction(addr, 'leave'))
|
|
173
|
|
174 return actions
|
|
175
|
|
176
|
|
177 def doEntranceMusic(self, action):
|
|
178 # these need to move out to their own service
|
|
179 dt = self.deltaSinceLastArrive(action['name'])
|
|
180 if dt > datetime.timedelta(hours=1):
|
|
181 import restkit, jsonlib
|
|
182 hub = restkit.Resource(
|
|
183 # PSHB not working yet; "http://bang:9030/"
|
|
184 "http://slash:9049/"
|
|
185 )
|
|
186 action = action.copy()
|
|
187 del action['created']
|
|
188 hub.post("visitorNet", payload=jsonlib.dumps(action))
|
|
189
|
|
190 def deltaSinceLastArrive(self, name):
|
|
191 results = list(self.mongo.find({'name' : name}).sort('created',
|
|
192 DESCENDING).limit(1))
|
|
193 if not results:
|
|
194 return datetime.timedelta.max
|
|
195 now = datetime.datetime.now(tz.gettz('UTC'))
|
|
196 last = results[0]['created'].replace(tzinfo=tz.gettz('UTC'))
|
|
197 return now - last
|
|
198
|
|
199
|
|
200 if __name__ == '__main__':
|
|
201 config = {
|
|
202 'servePort' : 9070,
|
|
203 'pollFrequency' : 1/5,
|
|
204 }
|
|
205 #log.startLogging(sys.stdout)
|
|
206
|
|
207
|
|
208 mongo = Connection('bang', 27017)['visitor']['visitor']
|
|
209
|
|
210 wifi = Wifi()
|
|
211 poller = Poller(wifi, mongo)
|
|
212 task.LoopingCall(poller.poll).start(1/config['pollFrequency'])
|
|
213
|
|
214 reactor.listenTCP(config['servePort'], Application(wifi, poller))
|
|
215 reactor.run()
|