807
|
1 #!/usr/bin/python
|
|
2
|
|
3 """
|
|
4 watch for bluetooth devices
|
|
5
|
|
6 this discoverer finds me if my treo has its screen on only, so I
|
|
7 have to wake up my own treo for a few seconds.
|
|
8
|
|
9 I can use 'hcitool cc <addr> && hcitool rssi <addr>' to wake it up and
|
|
10 get its signal strength, but that pattern crashes my treo easily. I
|
|
11 still don't have an access that wakes up the treo and then doesn't
|
|
12 crash it. Maybe I could pretend to be a headset or something.
|
|
13
|
|
14 depends on ubuntu package: python-bluez
|
|
15
|
|
16 """
|
|
17 from __future__ import absolute_import
|
|
18 import logging, time, datetime, restkit, jsonlib, cyclone.web, sys
|
|
19 from bluetooth import DeviceDiscoverer
|
|
20 from twisted.internet import reactor, defer, task
|
|
21 from rdflib.Graph import Graph
|
|
22 from rdflib import Literal, Variable, Namespace
|
|
23 from pymongo import Connection
|
|
24 from dateutil import tz
|
|
25
|
|
26 sys.path.append("/my/proj/homeauto/lib")
|
|
27 from cycloneerr import PrettyErrorHandler
|
|
28 from logsetup import log
|
|
29
|
|
30 mongo = Connection('bang', 27017)['visitor']['visitor']
|
|
31
|
|
32 ROOM = Namespace("http://projects.bigasterisk.com/room/")
|
|
33
|
|
34 class Disco(DeviceDiscoverer):
|
|
35 # it might be cool if this somehow returned
|
|
36 # _bt.EVT_INQUIRY_RESULT_WITH_RSSI: results. see
|
|
37 # /usr/share/pycentral/python-bluez/site-packages/bluetooth.py
|
|
38 def device_discovered(self, address, device_class, name):
|
|
39 log.debug("seeing: %s - %s (class 0x%X)" % (address, name, device_class))
|
|
40 self.nearby.append((address, name))
|
|
41
|
|
42 def inquiry_complete(self):
|
|
43 pass
|
|
44
|
|
45 def process_inquiry(self):
|
|
46 # more async version of the normal method
|
|
47 """
|
|
48 Starts calling process_event, returning a deferred that fires
|
|
49 when we're done.
|
|
50 """
|
|
51 self.done_inquiry = defer.Deferred()
|
|
52
|
|
53 if self.is_inquiring or len(self.names_to_find) > 0:
|
|
54 self.keep_processing()
|
|
55 else:
|
|
56 self.done_inquiry.callback(None)
|
|
57
|
|
58 return self.done_inquiry
|
|
59
|
|
60 def keep_processing(self):
|
|
61 # this one still blocks "a little bit"
|
|
62 if self.is_inquiring or len(self.names_to_find) > 0:
|
|
63 reactor.callLater(0, self.keep_processing)
|
|
64 log.debug("process_event()")
|
|
65 self.process_event() # <-- blocks here
|
|
66 else:
|
|
67 self.done_inquiry.callback(None)
|
|
68
|
|
69 def nearbyDevices(self):
|
|
70 """deferred to list of (addr,name) pairs"""
|
|
71 self.nearby = []
|
|
72 self.find_devices()
|
|
73 d = self.process_inquiry()
|
|
74 d.addCallback(lambda result: self.nearby)
|
|
75 return d
|
|
76
|
|
77 def devicesFromAddress(address):
|
|
78 for row in graph.query(
|
|
79 "SELECT ?dev { ?dev rm:bluetoothAddress ?addr }",
|
|
80 initNs=dict(rm=ROOM),
|
|
81 initBindings={Variable("?addr") : Literal(address)}):
|
|
82 (dev,) = row
|
|
83 yield dev
|
|
84
|
|
85 graph = Graph()
|
|
86 graph.parse("phones.n3", format="n3")
|
|
87
|
|
88 d = Disco()
|
|
89 hub = restkit.Resource(
|
|
90 # PSHB not working yet; "http://bang:9030/"
|
|
91 "http://slash:9049/"
|
|
92 )
|
|
93
|
|
94 def mongoInsert(msg):
|
|
95 try:
|
|
96 js = jsonlib.dumps(msg)
|
|
97 except UnicodeDecodeError:
|
|
98 pass
|
|
99 else:
|
|
100 if msg['name'] != 'THINKPAD_T43':
|
|
101 hub.post("visitorNet", payload=js) # sans datetime
|
|
102 msg['created'] = datetime.datetime.now(tz.gettz('UTC'))
|
|
103 mongo.insert(msg, safe=True)
|
|
104
|
|
105 class Poller(object):
|
|
106 def __init__(self):
|
|
107 self.lastDevs = set() # addresses
|
|
108 self.lastNameForAddress = {}
|
|
109 self.currentGraph = Graph()
|
|
110 self.lastPollTime = 0
|
|
111
|
|
112 def poll(self):
|
|
113 log.debug("get devices")
|
|
114 devs = d.nearbyDevices()
|
|
115
|
|
116 devs.addCallback(self.compare)
|
|
117 devs.addErrback(log.error)
|
|
118 return devs
|
|
119
|
|
120 def compare(self, newDevs):
|
|
121 self.lastPollTime = time.time()
|
|
122 log.debug("got: %r", newDevs)
|
|
123 lostDevs = self.lastDevs.copy()
|
|
124 prevDevs = self.lastDevs.copy()
|
|
125 self.lastDevs.clear()
|
|
126 stmts = []
|
|
127
|
|
128 for address, name in newDevs:
|
|
129 stmts.append((ROOM['bluetooth'],
|
|
130 ROOM['senses'],
|
|
131 Literal(str(address))))
|
|
132 if address not in prevDevs:
|
|
133 matches = 0
|
|
134 for dev in devicesFromAddress(address):
|
|
135 log.info("found %s" % dev)
|
|
136 matches += 1
|
|
137 if not matches:
|
|
138 log.info("no matches for %s (%s)" % (name, address))
|
|
139
|
|
140 print "%s %s %s" % (time.time(), name, address)
|
|
141
|
|
142 self.lastNameForAddress[address] = name
|
|
143 print 'mongoInsert', ({"sensor" : "bluetooth",
|
|
144 "address" : address,
|
|
145 "name" : name,
|
|
146 "action" : "arrive"})
|
|
147
|
|
148 lostDevs.discard(address)
|
|
149 self.lastDevs.add(address)
|
|
150
|
|
151 for address in lostDevs:
|
|
152 print 'mongoInsert', ({"sensor" : "bluetooth",
|
|
153 "address" : address,
|
|
154 "name" : self.lastNameForAddress[address],
|
|
155 "action" : "leave"})
|
|
156
|
|
157 for dev in devicesFromAddress(address):
|
|
158 log.info("lost %s" % dev)
|
|
159
|
|
160 class Index(PrettyErrorHandler, cyclone.web.RequestHandler):
|
|
161 def get(self):
|
|
162 age = time.time() - self.settings.poller.lastPollTime
|
|
163 if age > 60 + 30:
|
|
164 raise ValueError("poll data is stale. age=%s" % age)
|
|
165
|
|
166 self.write("bluetooth watcher. ")
|
|
167
|
|
168 if __name__ == '__main__':
|
|
169 log.setLevel(logging.DEBUG)
|
|
170 poller = Poller()
|
|
171 reactor.listenTCP(9077, cyclone.web.Application([
|
|
172 (r'/', Index),
|
|
173 ], poller=poller))
|
|
174 task.LoopingCall(poller.poll).start(1)
|
|
175 reactor.run()
|