comparison service/wifi/tomatoWifi.py @ 1223:34de6cfa0b6b

rename historical 'tomatoWifi' Ignore-this: 8a3f1f261df50e8029cf9de5b11a6896 darcs-hash:1b2345513349fd24e5b3992141f0b0f72ad43910
author drewp <drewp@bigasterisk.com>
date Sat, 30 Mar 2019 16:58:08 -0700
parents
children
comparison
equal deleted inserted replaced
1222:4ae655806141 1223:34de6cfa0b6b
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 """
14 from __future__ import division
15 import sys, cyclone.web, json, traceback, time, pystache, datetime, logging
16 import web.utils
17 from cyclone.httpclient import fetch
18
19 from dateutil import tz
20 from twisted.internet import reactor, task
21 from twisted.internet.defer import inlineCallbacks
22 import docopt
23 from influxdb import InfluxDBClient
24 from pymongo import MongoClient as Connection, DESCENDING
25 from rdflib import Namespace, Literal, URIRef, ConjunctiveGraph
26
27 from stategraph import StateGraph
28 from wifi import Wifi
29
30 from patchablegraph import PatchableGraph, CycloneGraphEventsHandler, CycloneGraphHandler
31
32 from rdfdb.patch import Patch
33
34 from cycloneerr import PrettyErrorHandler
35 from logsetup import log
36
37
38 DEV = Namespace("http://projects.bigasterisk.com/device/")
39 ROOM = Namespace("http://projects.bigasterisk.com/room/")
40 reasoning = "http://bang:9071/"
41
42 class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
43 def get(self):
44
45 age = time.time() - self.settings.poller.lastPollTime
46 if age > 10:
47 raise ValueError("poll data is stale. age=%s" % age)
48
49 self.set_header("Content-Type", "text/html")
50 self.write(open("index.html").read())
51
52 def whenConnected(mongo, macThatIsNowConnected):
53 lastArrive = None
54 for ev in mongo.find({'address': macThatIsNowConnected.upper()},
55 sort=[('created', -1)],
56 max_scan=100000):
57 if ev['action'] == 'arrive':
58 lastArrive = ev
59 if ev['action'] == 'leave':
60 break
61 if lastArrive is None:
62 raise ValueError("no past arrivals")
63
64 return lastArrive['created']
65
66 def connectedAgoString(conn):
67 return web.utils.datestr(
68 conn.astimezone(tz.tzutc()).replace(tzinfo=None))
69
70 class Table(PrettyErrorHandler, cyclone.web.RequestHandler):
71 def get(self):
72 def rowDict(row):
73 row['cls'] = "signal" if row.get('connected') else "nosignal"
74 if 'name' not in row:
75 row['name'] = row.get('clientHostname', '-')
76 if 'signal' not in row:
77 row['signal'] = 'yes' if row.get('connected') else 'no'
78
79 try:
80 conn = whenConnected(self.settings.mongo, row.get('mac', '??'))
81 row['connectedAgo'] = connectedAgoString(conn)
82 except ValueError:
83 row['connectedAgo'] = 'yes' if row.get('connected') else ''
84 row['router'] = row.get('ssid', '')
85 return row
86
87 self.set_header("Content-Type", "application/xhtml+xml")
88 self.write(pystache.render(
89 open("table.mustache").read(),
90 dict(
91 rows=sorted(map(rowDict, self.settings.poller.lastAddrs),
92 key=lambda a: (not a.get('connected'),
93 a.get('name'))))))
94
95
96 class Json(PrettyErrorHandler, cyclone.web.RequestHandler):
97 def get(self):
98 self.set_header("Content-Type", "application/json")
99 age = time.time() - self.settings.poller.lastPollTime
100 if age > 10:
101 raise ValueError("poll data is stale. age=%s" % age)
102 self.write(json.dumps({"wifi" : self.settings.poller.lastAddrs,
103 "dataAge" : age}))
104
105 class Poller(object):
106 def __init__(self, wifi, mongo):
107 self.wifi = wifi
108 self.mongo = mongo
109 self.lastAddrs = []
110 self.lastWithSignal = []
111 self.lastPollTime = 0
112
113 def assertCurrent(self):
114 dt = time.time() - self.lastPollTime
115 assert dt < 10, "last poll was %s sec ago" % dt
116
117 @inlineCallbacks
118 def poll(self):
119 connectedField = 'connected'
120 now = int(time.time())
121
122 # UVA mode:
123 addDhcpData = lambda *args: None
124
125 try:
126 newAddrs = yield self.wifi.getPresentMacAddrs()
127 addDhcpData(newAddrs)
128
129 newWithSignal = [a for a in newAddrs if a.get('connected')]
130
131 actions = self.computeActions(newWithSignal)
132 points = []
133 for action in actions:
134 log.info("action: %s", action)
135 action['created'] = datetime.datetime.now(tz.gettz('UTC'))
136 mongo.save(action)
137 points.append(
138 self.influxPoint(now, action['address'].lower(),
139 1 if action['action'] == 'arrive' else 0))
140 try:
141 self.doEntranceMusic(action)
142 except Exception, e:
143 log.error("entrancemusic error: %r", e)
144
145 if now // 3600 > self.lastPollTime // 3600:
146 log.info('hourly writes')
147 for addr in newWithSignal:
148 points.append(self.influxPoint(now, addr['mac'].lower(), 1))
149
150 influx.write_points(points, time_precision='s')
151 self.lastWithSignal = newWithSignal
152 if actions: # this doesn't currently include signal strength changes
153 fetch(reasoning + "immediateUpdate",
154 method='PUT',
155 timeout=2,
156 headers={'user-agent': ['tomatoWifi']}).addErrback(log.warn)
157 self.lastAddrs = newAddrs
158 self.lastPollTime = now
159
160 self.updateGraph(masterGraph)
161 except Exception, e:
162 log.error("poll error: %r\n%s", e, traceback.format_exc())
163
164 def influxPoint(self, now, address, value):
165 return {
166 'measurement': 'presence',
167 'tags': {'sensor': 'wifi', 'address': address,},
168 'fields': {'value': value},
169 'time': now,
170 }
171
172 def computeActions(self, newWithSignal):
173 actions = []
174
175 def makeAction(addr, act):
176 d = dict(sensor="wifi",
177 address=addr.get('mac').upper(), # mongo data is legacy uppercase
178 name=addr.get('name'),
179 networkName=addr.get('clientHostname'),
180 action=act)
181 if act == 'arrive' and 'ip' in addr:
182 # this won't cover the possible case that you get on
183 # wifi but don't have an ip yet. We'll record an
184 # action with no ip and then never record your ip.
185 d['ip'] = addr['ip']
186 return d
187
188 for addr in newWithSignal:
189 if addr['mac'] not in [r['mac'] for r in self.lastWithSignal]:
190 actions.append(makeAction(addr, 'arrive'))
191
192 for addr in self.lastWithSignal:
193 if addr['mac'] not in [r['mac'] for r in newWithSignal]:
194 actions.append(makeAction(addr, 'leave'))
195
196 return actions
197
198
199 # these need to move out to their own service
200 def doEntranceMusic(self, action):
201 import restkit, json
202 dt = self.deltaSinceLastArrive(action['name'])
203 log.debug("dt=%s", dt)
204 if dt > datetime.timedelta(hours=1):
205 hub = restkit.Resource(
206 # PSHB not working yet; "http://bang:9030/"
207 "http://slash:9049/"
208 )
209 action = action.copy()
210 del action['created']
211 del action['_id']
212 log.info("post to %s", hub)
213 hub.post("visitorNet", payload=json.dumps(action))
214
215 def deltaSinceLastArrive(self, name):
216 results = list(self.mongo.find({'name' : name}).sort('created',
217 DESCENDING).limit(1))
218 if not results:
219 return datetime.timedelta.max
220 now = datetime.datetime.now(tz.gettz('UTC'))
221 last = results[0]['created'].replace(tzinfo=tz.gettz('UTC'))
222 return now - last
223
224 def updateGraph(self, masterGraph):
225
226 g = ConjunctiveGraph()
227 ctx = DEV['wifi']
228
229 # someday i may also record specific AP and their strength,
230 # for positioning. But many users just want to know that the
231 # device is connected to some bigasterisk AP.
232 age = time.time() - self.lastPollTime
233 if age > 10:
234 raise ValueError("poll data is stale. age=%s" % age)
235
236 for dev in self.lastAddrs:
237 if not dev.get('connected'):
238 continue
239 uri = URIRef("http://bigasterisk.com/mac/%s" % dev['mac'].lower())
240 g.add((uri, ROOM['macAddress'], Literal(dev['mac'].lower()), ctx))
241
242 g.add((uri, ROOM['connected'], {
243 'wireless': URIRef("http://bigasterisk.com/wifiAccessPoints"),
244 '2.4G': URIRef("http://bigasterisk.com/wifiAccessPoints"),
245 '5G': URIRef("http://bigasterisk.com/wifiAccessPoints"),
246 '-': URIRef("http://bigasterisk.com/wifiUnknownConnectionType"),
247 'Unknown': URIRef("http://bigasterisk.com/wifiUnknownConnectionType"),
248 'wired': URIRef("http://bigasterisk.com/houseOpenNet")}[dev['contype']], ctx))
249 if 'clientHostname' in dev and dev['clientHostname']:
250 g.add((uri, ROOM['wifiNetworkName'], Literal(dev['clientHostname']), ctx))
251 if 'name' in dev and dev['name']:
252 g.add((uri, ROOM['deviceName'], Literal(dev['name']), ctx))
253 if 'signal' in dev:
254 g.add((uri, ROOM['signalStrength'], Literal(dev['signal']), ctx))
255 if 'model' in dev:
256 g.add((uri, ROOM['networkModel'], Literal(dev['model']), ctx))
257 try:
258 conn = whenConnected(mongo, dev['mac'])
259 except ValueError:
260 traceback.print_exc()
261 pass
262 else:
263 g.add((uri, ROOM['connectedAgo'],
264 Literal(connectedAgoString(conn)), ctx))
265 g.add((uri, ROOM['connected'], Literal(conn), ctx))
266 masterGraph.setToGraph(g)
267
268
269 if __name__ == '__main__':
270 args = docopt.docopt('''
271 Usage:
272 tomatoWifi [options]
273
274 Options:
275 -v, --verbose more logging
276 --port=<n> serve on port [default: 9070]
277 --poll=<freq> poll frequency [default: .2]
278 ''')
279 if args['--verbose']:
280 from twisted.python import log as twlog
281 twlog.startLogging(sys.stdout)
282 log.setLevel(10)
283 log.setLevel(logging.DEBUG)
284
285 mongo = Connection('bang', 27017, tz_aware=True)['visitor']['visitor']
286 influx = InfluxDBClient('bang', 9060, 'root', 'root', 'main')
287
288 masterGraph = PatchableGraph()
289 wifi = Wifi()
290 poller = Poller(wifi, mongo)
291 task.LoopingCall(poller.poll).start(1/float(args['--poll']))
292
293 reactor.listenTCP(int(args['--port']),
294 cyclone.web.Application(
295 [
296 (r"/", Index),
297 (r'/json', Json),
298 (r'/graph', CycloneGraphHandler, {'masterGraph': masterGraph}),
299 (r'/graph/events', CycloneGraphEventsHandler, {'masterGraph': masterGraph}),
300 (r'/table', Table),
301 #(r'/activity', Activity),
302 ],
303 wifi=wifi,
304 poller=poller,
305 mongo=mongo))
306 reactor.run()