changeset 358:7096dad37074

tradfri docker update Ignore-this: eccc13505e058527851714500d06a832
author drewp@bigasterisk.com
date Sat, 08 Sep 2018 02:26:23 -0700
parents b087642a456f
children b3959142d7d8
files service/tradfri/Dockerfile service/tradfri/makefile service/tradfri/requirements.txt service/tradfri/tradfri.py
diffstat 4 files changed, 242 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tradfri/Dockerfile	Sat Sep 08 02:26:23 2018 -0700
@@ -0,0 +1,14 @@
+FROM bang6:5000/base_x86
+
+WORKDIR /opt
+
+COPY requirements.txt ./
+RUN pip install -r requirements.txt
+
+COPY pytradfri pytradfri
+COPY libcoap libcoap
+COPY *.py req* ./
+
+EXPOSE 10009
+
+CMD [ "python", "./tradfri.py" ]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tradfri/makefile	Sat Sep 08 02:26:23 2018 -0700
@@ -0,0 +1,30 @@
+JOB=tradfri
+PORT=10009
+
+TAG=bang6:5000/${JOB}_x86:latest
+
+build_image:
+	rm -rf tmp_ctx
+	mkdir -p tmp_ctx
+	cp -a Dockerfile ../../lib/*.py *.py req* pytradfri libcoap tmp_ctx
+	docker build --network=host -t ${TAG} tmp_ctx
+	docker push ${TAG}
+	rm -rf tmp_ctx
+
+shell:
+	docker run --rm -it --cap-add SYS_PTRACE --net=host ${TAG} /bin/bash
+
+local_run:
+	docker run --rm -it -p ${PORT}:${PORT} \
+          -v `pwd`:/mnt \
+          --net=host \
+          ${TAG} \
+          python /mnt/${JOB}.py -v
+
+local_run_strace:
+	docker run --rm -it -p ${PORT}:${PORT} \
+          -v `pwd`:/mnt \
+          --net=host \
+          --cap-add SYS_PTRACE \
+          ${TAG} \
+          strace -f -tts 200 python /mnt/${JOB}.py -v
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tradfri/requirements.txt	Sat Sep 08 02:26:23 2018 -0700
@@ -0,0 +1,8 @@
+twisted
+cyclone
+docopt
+python-dateutil
+rdflib==4.2.2
+rdflib-jsonld==0.4.0
+
+https://projects.bigasterisk.com/rdfdb/rdfdb-0.3.0.tar.gz
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/tradfri/tradfri.py	Sat Sep 08 02:26:23 2018 -0700
@@ -0,0 +1,190 @@
+from __future__ import division
+import sys, logging, socket, json, time, os, traceback
+import cyclone.web
+from cyclone.httpclient import fetch
+from rdflib import Namespace, URIRef, Literal, Graph, RDF, RDFS, ConjunctiveGraph
+from rdflib.parser import StringInputSource
+from twisted.internet import reactor, task
+from docopt import docopt
+import logging
+logging.basicConfig(level=logging.DEBUG)
+sys.path.append("/opt")
+from patchablegraph import PatchableGraph, CycloneGraphHandler, CycloneGraphEventsHandler
+from rdfdb.rdflibpatch import inContext
+from rdfdb.patch import Patch
+from dateutil.tz import tzlocal, tzutc
+import private
+
+sys.path.append('pytradfri')
+from pytradfri import Gateway
+from pytradfri.api.libcoap_api import APIFactory
+from pytradfri.const import ATTR_LIGHT_STATE, ATTR_LIGHT_DIMMER
+
+ROOM = Namespace('http://projects.bigasterisk.com/room/')
+IKEADEV = Namespace('http://bigasterisk.com/ikeaDevice/')
+log = logging.getLogger()
+
+def devUri(dev):
+    name = dev.name if dev.name else dev.id
+    return IKEADEV['_'.join(w.lower() for w in name.split())]
+
+class Hub(object):
+    def __init__(self, graph, ip, key):
+        self.graph = graph
+        self.ip, self.key = ip, key
+        self.api = APIFactory(ip, psk=key)
+        self.gateway = Gateway()
+
+        devices_command = self.gateway.get_devices()
+        self.devices_commands = self.api.request(devices_command)
+        self.devices = self.api.request(self.devices_commands)
+
+        self.ctx = ROOM['tradfriHub']
+        self.graph.patch(Patch(
+            addQuads=[(s,p,o,self.ctx) for s,p,o in self.deviceStatements()]))
+
+        self.curStmts = []
+
+        task.LoopingCall(self.updateCur).start(60)
+        for dev in self.devices:
+            self.startObserve(dev)
+            
+    def startObserve(self, dev):
+        def onUpdate(dev):
+            reactor.callFromThread(self.updateCur, dev)
+        def onErr(err):
+            log.warn('%r; restart observe on %r', err, dev)
+            reactor.callLater(1, self.startObserve, dev)
+        reactor.callInThread(self.api.request, dev.observe(onUpdate, onErr))
+
+    def description(self):
+        return {
+            'uri': 'huburi',
+            'devices': [{
+                'uri': devUri(dev),
+                'className': self.__class__.__name__,
+                'pinNumber': None,
+                'outputPatterns': [(devUri(dev), ROOM['brightness'], None)],
+                'watchPrefixes': [],
+                'outputWidgets': [{
+                    'element': 'output-slider',
+                    'min': 0, 'max': 1, 'step': 1 / 255,
+                    'subj': devUri(dev),
+                    'pred': ROOM['brightness'],
+                }] * dev.has_light_control,
+            } for dev in self.devices],
+            'graph': 'http://sticker:9059/graph', #todo
+        }
+        
+    def updateCur(self, dev=None):
+        cur = [(s,p,o,self.ctx) for s,p,o in
+               self.currentStateStatements([dev] if dev else self.devices)]
+        self.graph.patch(Patch(addQuads=cur, delQuads=self.curStmts))
+        self.curStmts = cur
+                
+    def deviceStatements(self):
+        for dev in self.devices:
+            uri = devUri(dev)
+            yield (uri, RDF.type, ROOM['IkeaDevice'])
+            yield (uri, ROOM['ikeaId'], Literal(dev.id))
+            if dev.last_seen:
+                utcSeen = dev.last_seen
+                yield (uri, ROOM['lastSeen'],
+                       Literal(utcSeen.replace(tzinfo=tzutc()).astimezone(tzlocal())))
+                yield (uri, ROOM['reachable'], ROOM['yes'] if dev.reachable else ROOM['no'])
+            yield (uri, RDFS.label, Literal(dev.name))
+            # no connection between remotes and lights?
+            
+    def currentStateStatements(self, devs):
+        for dev in self.devices:  # could scan just devs, but the Patch line needs a fix
+            uri = devUri(dev)
+            di = dev.device_info
+            if di.battery_level is not None:
+                yield (uri, ROOM['batteryLevel'], Literal(di.battery_level / 100))
+            if dev.has_light_control:
+                lc = dev.light_control
+                #import ipdb;ipdb.set_trace()
+
+                lightUri = devUri(dev)
+                print lc.raw
+                if not lc.raw[0][ATTR_LIGHT_STATE]:
+                    level = 0
+                else:
+                    level = lc.raw[0][ATTR_LIGHT_DIMMER] / 255
+                yield (lightUri, ROOM['brightness'], Literal(level))
+                #if light.hex_color:
+                #    yield (lightUri, ROOM['kelvinColor'], Literal(light.kelvin_color))
+                #    yield (lightUri, ROOM['color'], Literal('#%s' % light.hex_color))
+            
+
+    def outputStatements(self, stmts):
+        for stmt in stmts:
+            for dev in self.devices:
+                uri = devUri(dev)
+                if stmt[0] == uri:
+                    if stmt[1] == ROOM['brightness']:
+                        try:
+                            self.api.request(dev.light_control.set_dimmer(
+                                int(255 * float(stmt[2])), transition_time=3))
+                        except:
+                            traceback.print_exc()
+                            raise
+        self.updateCur()
+      
+class OutputPage(cyclone.web.RequestHandler):
+    def put(self):
+        arg = self.request.arguments
+        if arg.get('s') and arg.get('p'):
+            subj = URIRef(arg['s'][-1])
+            pred = URIRef(arg['p'][-1])
+            turtleLiteral = self.request.body
+            try:
+                obj = Literal(float(turtleLiteral))
+            except ValueError:
+                obj = Literal(turtleLiteral)
+            stmt = (subj, pred, obj)
+        else:
+            g = rdfGraphBody(self.request.body, self.request.headers)
+            assert len(g) == 1, len(g)
+            stmt = g.triples((None, None, None)).next()
+
+        self.settings.hub.outputStatements([stmt])
+
+hostname = socket.gethostname()
+
+class Boards(cyclone.web.RequestHandler):
+    def get(self):
+        self.set_header('Content-type', 'application/json')
+        self.write(json.dumps({
+            'host': hostname,
+            'boards': [self.settings.hub.description()]
+        }, indent=2))
+        
+def main():
+    arg = docopt("""
+    Usage: tradfri.py [options]
+
+    -v   Verbose
+    """)
+    log.setLevel(logging.WARN)
+    if arg['-v']:
+        from twisted.python import log as twlog
+        twlog.startLogging(sys.stdout)
+        log.setLevel(logging.DEBUG)
+
+    masterGraph = PatchableGraph()
+    hub = Hub(masterGraph, private.hubAddr, key=private.hubKey)
+    
+    reactor.listenTCP(10009, cyclone.web.Application([
+        (r"/()", cyclone.web.StaticFileHandler, {
+            "path": "../arduinoNode/static", "default_filename": "index.html"}),
+        (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "../arduinoNode/static"}),
+        (r'/boards', Boards),
+        (r"/graph", CycloneGraphHandler, {'masterGraph': masterGraph}),
+        (r"/graph/events", CycloneGraphEventsHandler, {'masterGraph': masterGraph}),
+        (r'/output', OutputPage),
+        ], hub=hub, debug=arg['-v']), interface='::')
+    log.warn('serving on 10009')
+    reactor.run()
+
+main()