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