Mercurial > code > home > repos > homeauto
annotate service/tradfri/tradfri.py @ 723:b87b6e9cedb2
whitespace
Ignore-this: c727f388f197a6fae88595fe6d455c7d
author | drewp@bigasterisk.com |
---|---|
date | Wed, 05 Feb 2020 00:29:13 -0800 |
parents | a93fbf0d0daa |
children | cd77bcbfd522 |
rev | line source |
---|---|
358 | 1 from __future__ import division |
2 import sys, logging, socket, json, time, os, traceback | |
3 import cyclone.web | |
4 from cyclone.httpclient import fetch | |
5 from rdflib import Namespace, URIRef, Literal, Graph, RDF, RDFS, ConjunctiveGraph | |
6 from rdflib.parser import StringInputSource | |
7 from twisted.internet import reactor, task | |
8 from docopt import docopt | |
9 import logging | |
10 logging.basicConfig(level=logging.DEBUG) | |
11 sys.path.append("/opt") | |
12 from patchablegraph import PatchableGraph, CycloneGraphHandler, CycloneGraphEventsHandler | |
13 from rdfdb.rdflibpatch import inContext | |
14 from rdfdb.patch import Patch | |
15 from dateutil.tz import tzlocal, tzutc | |
16 import private | |
17 | |
18 sys.path.append('pytradfri') | |
19 from pytradfri import Gateway | |
20 from pytradfri.api.libcoap_api import APIFactory | |
21 from pytradfri.const import ATTR_LIGHT_STATE, ATTR_LIGHT_DIMMER | |
22 | |
23 ROOM = Namespace('http://projects.bigasterisk.com/room/') | |
24 IKEADEV = Namespace('http://bigasterisk.com/ikeaDevice/') | |
25 log = logging.getLogger() | |
26 | |
27 def devUri(dev): | |
28 name = dev.name if dev.name else dev.id | |
29 return IKEADEV['_'.join(w.lower() for w in name.split())] | |
30 | |
31 class Hub(object): | |
32 def __init__(self, graph, ip, key): | |
33 self.graph = graph | |
34 self.ip, self.key = ip, key | |
35 self.api = APIFactory(ip, psk=key) | |
36 self.gateway = Gateway() | |
37 | |
38 devices_command = self.gateway.get_devices() | |
39 self.devices_commands = self.api.request(devices_command) | |
40 self.devices = self.api.request(self.devices_commands) | |
41 | |
42 self.ctx = ROOM['tradfriHub'] | |
43 self.graph.patch(Patch( | |
44 addQuads=[(s,p,o,self.ctx) for s,p,o in self.deviceStatements()])) | |
45 | |
46 self.curStmts = [] | |
47 | |
48 task.LoopingCall(self.updateCur).start(60) | |
49 for dev in self.devices: | |
50 self.startObserve(dev) | |
723 | 51 |
358 | 52 def startObserve(self, dev): |
53 def onUpdate(dev): | |
54 reactor.callFromThread(self.updateCur, dev) | |
55 def onErr(err): | |
56 log.warn('%r; restart observe on %r', err, dev) | |
57 reactor.callLater(1, self.startObserve, dev) | |
58 reactor.callInThread(self.api.request, dev.observe(onUpdate, onErr)) | |
59 | |
60 def description(self): | |
61 return { | |
62 'uri': 'huburi', | |
63 'devices': [{ | |
64 'uri': devUri(dev), | |
65 'className': self.__class__.__name__, | |
66 'pinNumber': None, | |
67 'outputPatterns': [(devUri(dev), ROOM['brightness'], None)], | |
68 'watchPrefixes': [], | |
69 'outputWidgets': [{ | |
70 'element': 'output-slider', | |
71 'min': 0, 'max': 1, 'step': 1 / 255, | |
72 'subj': devUri(dev), | |
73 'pred': ROOM['brightness'], | |
74 }] * dev.has_light_control, | |
75 } for dev in self.devices], | |
76 'graph': 'http://sticker:9059/graph', #todo | |
77 } | |
723 | 78 |
358 | 79 def updateCur(self, dev=None): |
80 cur = [(s,p,o,self.ctx) for s,p,o in | |
81 self.currentStateStatements([dev] if dev else self.devices)] | |
82 self.graph.patch(Patch(addQuads=cur, delQuads=self.curStmts)) | |
83 self.curStmts = cur | |
723 | 84 |
358 | 85 def deviceStatements(self): |
86 for dev in self.devices: | |
87 uri = devUri(dev) | |
88 yield (uri, RDF.type, ROOM['IkeaDevice']) | |
89 yield (uri, ROOM['ikeaId'], Literal(dev.id)) | |
90 if dev.last_seen: | |
91 utcSeen = dev.last_seen | |
92 yield (uri, ROOM['lastSeen'], | |
93 Literal(utcSeen.replace(tzinfo=tzutc()).astimezone(tzlocal()))) | |
94 yield (uri, ROOM['reachable'], ROOM['yes'] if dev.reachable else ROOM['no']) | |
95 yield (uri, RDFS.label, Literal(dev.name)) | |
96 # no connection between remotes and lights? | |
723 | 97 |
358 | 98 def currentStateStatements(self, devs): |
99 for dev in self.devices: # could scan just devs, but the Patch line needs a fix | |
100 uri = devUri(dev) | |
101 di = dev.device_info | |
102 if di.battery_level is not None: | |
103 yield (uri, ROOM['batteryLevel'], Literal(di.battery_level / 100)) | |
104 if dev.has_light_control: | |
105 lc = dev.light_control | |
106 #import ipdb;ipdb.set_trace() | |
107 | |
108 lightUri = devUri(dev) | |
109 print lc.raw | |
110 if not lc.raw[0][ATTR_LIGHT_STATE]: | |
111 level = 0 | |
112 else: | |
113 level = lc.raw[0][ATTR_LIGHT_DIMMER] / 255 | |
114 yield (lightUri, ROOM['brightness'], Literal(level)) | |
115 #if light.hex_color: | |
116 # yield (lightUri, ROOM['kelvinColor'], Literal(light.kelvin_color)) | |
117 # yield (lightUri, ROOM['color'], Literal('#%s' % light.hex_color)) | |
723 | 118 |
358 | 119 |
120 def outputStatements(self, stmts): | |
121 for stmt in stmts: | |
122 for dev in self.devices: | |
123 uri = devUri(dev) | |
124 if stmt[0] == uri: | |
125 if stmt[1] == ROOM['brightness']: | |
126 try: | |
127 self.api.request(dev.light_control.set_dimmer( | |
128 int(255 * float(stmt[2])), transition_time=3)) | |
129 except: | |
130 traceback.print_exc() | |
131 raise | |
132 self.updateCur() | |
723 | 133 |
358 | 134 class OutputPage(cyclone.web.RequestHandler): |
135 def put(self): | |
136 arg = self.request.arguments | |
137 if arg.get('s') and arg.get('p'): | |
138 subj = URIRef(arg['s'][-1]) | |
139 pred = URIRef(arg['p'][-1]) | |
140 turtleLiteral = self.request.body | |
141 try: | |
142 obj = Literal(float(turtleLiteral)) | |
143 except ValueError: | |
144 obj = Literal(turtleLiteral) | |
145 stmt = (subj, pred, obj) | |
146 else: | |
147 g = rdfGraphBody(self.request.body, self.request.headers) | |
148 assert len(g) == 1, len(g) | |
149 stmt = g.triples((None, None, None)).next() | |
150 | |
151 self.settings.hub.outputStatements([stmt]) | |
152 | |
153 hostname = socket.gethostname() | |
154 | |
155 class Boards(cyclone.web.RequestHandler): | |
156 def get(self): | |
157 self.set_header('Content-type', 'application/json') | |
158 self.write(json.dumps({ | |
159 'host': hostname, | |
160 'boards': [self.settings.hub.description()] | |
161 }, indent=2)) | |
723 | 162 |
358 | 163 def main(): |
164 arg = docopt(""" | |
165 Usage: tradfri.py [options] | |
166 | |
167 -v Verbose | |
168 """) | |
169 log.setLevel(logging.WARN) | |
170 if arg['-v']: | |
171 from twisted.python import log as twlog | |
172 twlog.startLogging(sys.stdout) | |
173 log.setLevel(logging.DEBUG) | |
174 | |
175 masterGraph = PatchableGraph() | |
176 hub = Hub(masterGraph, private.hubAddr, key=private.hubKey) | |
723 | 177 |
358 | 178 reactor.listenTCP(10009, cyclone.web.Application([ |
179 (r"/()", cyclone.web.StaticFileHandler, { | |
364 | 180 "path": "/opt/static", "default_filename": "index.html"}), |
181 (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "/opt/static"}), | |
358 | 182 (r'/boards', Boards), |
722
a93fbf0d0daa
dep updates; graph url renames; and other build updates
drewp@bigasterisk.com
parents:
364
diff
changeset
|
183 (r"/graph/tradfri", CycloneGraphHandler, {'masterGraph': masterGraph}), |
a93fbf0d0daa
dep updates; graph url renames; and other build updates
drewp@bigasterisk.com
parents:
364
diff
changeset
|
184 (r"/graph/tradfri/events", CycloneGraphEventsHandler, {'masterGraph': masterGraph}), |
358 | 185 (r'/output', OutputPage), |
186 ], hub=hub, debug=arg['-v']), interface='::') | |
187 log.warn('serving on 10009') | |
188 reactor.run() | |
189 | |
190 main() |