Mercurial > code > home > repos > homeauto
annotate service/beacon/beaconmap.py @ 1423:ba56263fe3b2
arduinonode to docker
Ignore-this: 8f689c7491819bc47200018b517fd7de
darcs-hash:e3602fc781c7b66e98ca950d5782ecc41e506bad
author | drewp <drewp@bigasterisk.com> |
---|---|
date | Wed, 07 Aug 2019 20:23:04 -0700 |
parents | f38fb6956d8e |
children |
rev | line source |
---|---|
1096 | 1 from __future__ import division |
2 import sys, cyclone.web, json, datetime, time | |
3 import arrow | |
4 from twisted.internet import reactor, task | |
5 from dateutil.tz import tzlocal | |
6 import math | |
7 import cyclone.sse | |
8 from locator import Locator, Measurement | |
9 | |
10 sys.path.append("/my/proj/homeauto/lib") | |
11 from cycloneerr import PrettyErrorHandler | |
12 from logsetup import log | |
13 | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
14 from db import Db |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
15 |
1096 | 16 class Devices(PrettyErrorHandler, cyclone.web.RequestHandler): |
17 def get(self): | |
18 devices = [] | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
19 startCount = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*20) |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
20 for addr in db.addrs(startCount): |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
21 # limit to "addr_type": "Public" |
1096 | 22 name = None |
23 if addr == "00:ea:23:23:c6:c4": | |
24 name = 'apollo' | |
25 if addr == "00:ea:23:21:e0:a4": | |
26 name = 'white' | |
27 if addr == "00:ea:23:24:f8:d4": | |
28 name = 'green' | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
29 row = db.latestDetail(addr) |
1096 | 30 if 'Eddystone-URL' in row: |
31 name = row['Eddystone-URL'] | |
32 devices.append({ | |
33 'addr': addr, | |
34 'name': name}) | |
35 devices.sort(key=lambda d: (d['name'] or 'zzz', | |
36 d['addr'])) | |
37 self.set_header("Content-Type", "application/json") | |
38 self.write(json.dumps({'devices': devices})) | |
39 | |
40 | |
41 class Poller(object): | |
42 def __init__(self): | |
43 self.listeners = [] # Points handlers | |
44 | |
45 self.lastPointTime = {} # addr : secs | |
46 self.lastValues = {} # addr : {sensor: (secs, rssi)} | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
47 task.LoopingCall(self.poll).start(2) |
1096 | 48 |
49 def poll(self): | |
50 addrs = set(l.addr for l in self.listeners if l.addr) | |
51 seconds = 60 * 20 | |
52 now = datetime.datetime.now(tzlocal()) | |
53 startTime = (now - datetime.timedelta(seconds=seconds)) | |
54 startTimeSec = arrow.get(startTime).timestamp | |
55 for addr in addrs: | |
56 points = {} # from: [offsetSec, rssi] | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
57 for row in db.recentRssi(startTime, addr): |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
58 t = (row['time'] - startTime).total_seconds() |
1096 | 59 points.setdefault(row['from'], []).append([ |
60 round(t, 2), row['rssi']]) | |
61 self.lastValues.setdefault(addr, {})[row['from']] = ( | |
62 now, row['rssi']) | |
63 | |
64 for pts in points.values(): | |
65 smooth(pts) | |
66 | |
67 if not points: | |
68 continue | |
69 | |
70 last = max(pts[-1][0] + startTimeSec for pts in points.values()) | |
71 if self.lastPointTime.get(addr, 0) == last: | |
72 continue | |
73 self.lastPointTime[addr] = last | |
74 msg = json.dumps({ | |
75 'addr': addr, | |
76 'startTime': startTimeSec, | |
77 'points': [{'from': k, 'points': v} | |
78 for k,v in sorted(points.items())]}) | |
79 for lis in self.listeners: | |
80 if lis.addr == addr: | |
81 lis.sendEvent(msg) | |
82 | |
83 def lastValue(self, addr, maxSensorAgeSec=30): | |
84 """note: only considers actively polled addrs""" | |
85 out = {} # from: rssi | |
86 now = datetime.datetime.now(tzlocal()) | |
87 for sensor, (t, rssi) in self.lastValues.get(addr, {}).iteritems(): | |
88 print 'consider %s %s' % (t, now) | |
89 if (now - t).total_seconds() < maxSensorAgeSec: | |
90 out[sensor] = rssi | |
91 return out | |
92 | |
93 def smooth(pts): | |
94 # see https://filterpy.readthedocs.io/en/latest/kalman/UnscentedKalmanFilter.html | |
95 for i in range(0, len(pts)): | |
96 if i == 0: | |
97 prevT, smoothX = pts[i] | |
98 else: | |
99 t, x = pts[i] | |
100 if t - prevT < 30: | |
101 smoothX = .8 * smoothX + .2 * x | |
102 else: | |
103 smoothX = x | |
104 pts[i] = [t, round(smoothX, 1)] | |
105 prevT = t | |
106 | |
107 class Points(cyclone.sse.SSEHandler): | |
108 def __init__(self, application, request, **kw): | |
109 cyclone.sse.SSEHandler.__init__(self, application, request, **kw) | |
110 if request.headers['accept'] != 'text/event-stream': | |
111 raise ValueError('ignoring bogus request') | |
112 self.addr = request.arguments.get('addr', [None])[0] | |
113 | |
114 def bind(self): | |
115 if not self.addr: | |
116 return | |
117 poller.listeners.append(self) | |
118 def unbind(self): | |
119 if not self.addr: | |
120 return | |
121 poller.listeners.remove(self) | |
122 | |
123 class LocatorEstimatesPoller(object): | |
124 def __init__(self): | |
125 self.listeners = [] | |
126 self.lastResult = {} | |
127 self.locator = Locator() | |
128 task.LoopingCall(self.poll).start(1) | |
129 | |
130 def poll(self): | |
131 addrs = set(l.addr for l in self.listeners if l.addr) | |
132 now = datetime.datetime.now(tzlocal()) | |
133 cutoff = (now - datetime.timedelta(seconds=60)) | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
134 |
1096 | 135 for addr in addrs: |
136 d = {} # from: [(t, rssi)] | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
137 for row in db.recentRssi(cutoff, addr): |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
138 d.setdefault(row['from'], []).append((row['time'].timestamp, row['rssi'])) |
1096 | 139 |
140 for pts in d.values(): | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
141 pts.sort() |
1096 | 142 smooth(pts) |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
143 |
1096 | 144 meas = Measurement(dict((k, v[-1][1]) for k, v in d.items())) |
145 nearest = [ | |
146 (dist, coord) for dist, coord in self.locator.nearestPoints(meas) if dist < 25 | |
147 ] | |
148 if nearest: | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
149 weightedCoord = self.locator.estimatePosition(nearest) |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
150 else: |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
151 weightedCoord = [-999, -999, -999] |
1096 | 152 self.lastResult[addr] = {'nearest': nearest, 'weightedCoord': weightedCoord} |
153 | |
154 for lis in self.listeners: | |
155 lis.sendEvent(self.lastResult[addr]) | |
156 | |
157 class PositionEstimates(cyclone.sse.SSEHandler): | |
158 def __init__(self, application, request, **kw): | |
159 cyclone.sse.SSEHandler.__init__(self, application, request, **kw) | |
160 if request.headers['accept'] != 'text/event-stream': | |
161 raise ValueError('ignoring bogus request') | |
162 self.addr = request.arguments.get('addr', [None])[0] | |
163 | |
164 def bind(self): | |
165 if not self.addr: | |
166 return | |
167 locatorEstimatesPoller.listeners.append(self) | |
168 def unbind(self): | |
169 if not self.addr: | |
170 return | |
171 locatorEstimatesPoller.listeners.remove(self) | |
172 | |
173 class Sensors(PrettyErrorHandler, cyclone.web.RequestHandler): | |
174 def get(self): | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
175 sensors = db.sensors() |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
176 |
1096 | 177 t1 = datetime.datetime.now(tzlocal()) - datetime.timedelta(seconds=60*10) |
178 out = [] | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
179 |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
180 allRows = list(db.recentRssi(t1)) |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
181 allRows.sort(key=lambda r: r['time'], reverse=True) |
1096 | 182 |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
183 for sens in sensors: |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
184 rssiHist = {} # level: count |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
185 for row in allRows: |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
186 if row['from'] == sens: |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
187 bucket = (row['rssi'] // 5) * 5 |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
188 rssiHist[bucket] = rssiHist.get(bucket, 0) + 1 |
1096 | 189 |
190 out.append({ | |
191 'from': sens, | |
192 'count': sum(rssiHist.values()), | |
193 'hist': rssiHist, | |
194 }) | |
195 | |
196 self.set_header("Content-Type", "application/json") | |
197 self.write(json.dumps({'sensors': out})) | |
198 | |
199 class Save(PrettyErrorHandler, cyclone.web.RequestHandler): | |
200 def post(self): | |
201 lines = open('saved_points').readlines() | |
202 lineNum = len(lines) + 1 | |
203 row = poller.lastValue('00:ea:23:21:e0:a4') | |
204 with open('saved_points', 'a') as out: | |
205 out.write('%s %r\n' % (lineNum, row)) | |
206 self.write('wrote line %s: %r' % (lineNum, row)) | |
1115
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
207 |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
208 db = Db() |
f38fb6956d8e
beaconmap rewrites with influxdb
drewp <drewp@bigasterisk.com>
parents:
1096
diff
changeset
|
209 |
1096 | 210 poller = Poller() |
211 locatorEstimatesPoller = LocatorEstimatesPoller() | |
212 | |
213 reactor.listenTCP( | |
214 9113, | |
215 cyclone.web.Application([ | |
216 (r"/(|.*\.(?:js|html|json))$", cyclone.web.StaticFileHandler, { | |
217 "path": ".", "default_filename": "beaconmap.html"}), | |
218 (r"/devices", Devices), | |
219 (r'/points', Points), | |
220 (r'/sensors', Sensors), | |
221 (r'/save', Save), | |
222 (r'/positionEstimates', PositionEstimates), | |
223 ])) | |
224 log.info('serving on 9113') | |
225 reactor.run() |