changeset 1506:37cbb245d93c

fix tests. add logging, some mypy types. Ignore-this: 61371f65438a4e77f70d21cc5d5193bf
author Drew Perttula <drewp@bigasterisk.com>
date Tue, 28 Mar 2017 07:48:10 +0000
parents e917fb4eea3a
children b246a5262d1e
files light9/collector/collector.py light9/collector/collector_test.py light9/collector/device.py light9/collector/device_test.py light9/collector/output.py light9/collector/output_test.py light9/io/__init__.py light9/rdfdb/graphfile_test.py makefile requirements.txt
diffstat 10 files changed, 167 insertions(+), 155 deletions(-) [+]
line wrap: on
line diff
--- a/light9/collector/collector.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/collector/collector.py	Tue Mar 28 07:48:10 2017 +0000
@@ -2,13 +2,22 @@
 import time
 import logging
 from rdflib import Literal
-from light9.namespaces import L9, RDF, DEV
+from light9.namespaces import L9, RDF
 from light9.collector.output import setListElem
 from light9.collector.device import toOutputAttrs, resolve
 
+# types only
+from rdflib import Graph, URIRef
+from typing import List, Dict, Tuple, Any, TypeVar, Generic
+from light9.collector.output import Output
+
+ClientType = TypeVar('ClientType')
+ClientSessionType = TypeVar('ClientSessionType')
+
 log = logging.getLogger('collector')
 
 def outputMap(graph, outputs):
+    # type: (Graph, List[Output]) -> Dict[Tuple[URIRef, URIRef], Tuple[Output, int]]
     """From rdf config graph, compute a map of
        (device, outputattr) : (output, index)
     that explains which output index to set for any device update.
@@ -20,26 +29,38 @@
         outputByUri[out.uri] = out
 
     for dc in graph.subjects(RDF.type, L9['DeviceClass']):
+        log.info('mapping DeviceClass %s', dc)
         for dev in graph.subjects(RDF.type, dc):
-            output = outputByUri[graph.value(dev, L9['dmxUniverse'])]
+            log.info('  mapping device %s', dev)
+            universe = graph.value(dev, L9['dmxUniverse'])
+            try:
+                output = outputByUri[universe]
+            except Exception:
+                log.warn('dev %r :dmxUniverse %r', dev, universe)
+                raise
             dmxBase = int(graph.value(dev, L9['dmxBase']).toPython())
             for row in graph.objects(dc, L9['attr']):
                 outputAttr = graph.value(row, L9['outputAttr'])
                 offset = int(graph.value(row, L9['dmxOffset']).toPython())
                 index = dmxBase + offset - 1
                 ret[(dev, outputAttr)] = (output, index)
-                log.info('map %s,%s to %s,%s', dev, outputAttr, output, index)
+                log.info('    map %s to %s,%s', outputAttr, output, index)
     return ret
         
-class Collector(object):
+class Collector(Generic[ClientType, ClientSessionType]):
     def __init__(self, graph, outputs, clientTimeoutSec=10):
+        # type: (Graph, List[Output], float) -> None
         self.graph = graph
         self.outputs = outputs
         self.clientTimeoutSec = clientTimeoutSec
 
         self.graph.addHandler(self.rebuildOutputMap)
-        self.lastRequest = {} # client : (session, time, {(dev,devattr): latestValue})
-        self.stickyAttrs = {} # (dev, devattr): value to use instead of 0
+
+        # client : (session, time, {(dev,devattr): latestValue})
+        self.lastRequest = {} # type: Dict[ClientType, Tuple[ClientSessionType, float, Dict[Tuple[URIRef, URIRef], float]]]
+
+        # (dev, devAttr): value to use instead of 0
+        self.stickyAttrs = {} # type: Dict[Tuple[URIRef, URIRef], float] 
 
     def rebuildOutputMap(self):
         self.outputMap = outputMap(self.graph, self.outputs) # (device, outputattr) : (output, index)
@@ -56,15 +77,18 @@
                     self.remapOut[(dev, attr)] = start, end
 
     def _forgetStaleClients(self, now):
+        # type: (float) -> None
         staleClients = []
-        for c, (_, t, _) in self.lastRequest.iteritems():
+        for c, (_, t, _2) in self.lastRequest.iteritems():
             if t < now - self.clientTimeoutSec:
                 staleClients.append(c)
         for c in staleClients:
+            log.info('forgetting stale client %r', c)
             del self.lastRequest[c]
 
     def resolvedSettingsDict(self, settingsList):
-        out = {}
+        # type: (List[Tuple[URIRef, URIRef, float]]) -> Dict[Tuple[URIRef, URIRef], float]
+        out = {} # type: Dict[Tuple[URIRef, URIRef], float]
         for d, da, v in settingsList:
             if (d, da) in out:
                 out[(d, da)] = resolve(d, da, [out[(d, da)], v])
@@ -72,6 +96,12 @@
                 out[(d, da)] = v
         return out
 
+    def _warnOnLateRequests(self, client, now, sendTime):
+        requestLag = now - sendTime
+        if requestLag > .1:
+            log.warn('collector.setAttrs from %s is running %.1fms after the request was made',
+                     client, requestLag * 1000)
+        
     def setAttrs(self, client, clientSession, settings, sendTime):
         """
         settings is a list of (device, attr, value). These attrs are
@@ -82,10 +112,7 @@
         Call with settings=[] to ping us that your session isn't dead.
         """
         now = time.time()
-        requestLag = now - sendTime
-        if requestLag > .1:
-            log.warn('collector.setAttrs from %s is running %.1fms after the request was made',
-                     client, requestLag * 1000)
+        self._warnOnLateRequests(client, now, sendTime)
 
         self._forgetStaleClients(now)
 
@@ -128,6 +155,7 @@
         pendingOut = {} # output : values
         for out in self.outputs:
             pendingOut[out] = [0] * out.numChannels
+
         for device, attrs in outputAttrs.iteritems():
             for outputAttr, value in attrs.iteritems():
                 self.setAttr(device, outputAttr, value, pendingOut)
--- a/light9/collector/collector_test.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/collector/collector_test.py	Tue Mar 28 07:48:10 2017 +0000
@@ -1,7 +1,7 @@
 import unittest
-import datetime
+import datetime, time
 from freezegun import freeze_time
-from rdflib import Namespace
+from rdflib import Namespace, URIRef
 
 from light9.namespaces import L9, DEV
 from light9.collector.collector import Collector, outputMap
@@ -17,10 +17,32 @@
         @prefix dmx0: <http://light9.bigasterisk.com/output/dmx0/> .
 '''
 
+THEATER = '''
+        :brightness         a :DeviceAttr; :dataType :scalar .
+
+        :SimpleDimmer a :DeviceClass;
+          :deviceAttr :brightness;
+          :attr
+            [ :outputAttr :level; :dmxOffset 0 ] .
+                
+        :ChauvetColorStrip a :DeviceClass;
+          :deviceAttr :color;
+          :attr
+            [ :outputAttr :mode;  :dmxOffset 0 ],
+            [ :outputAttr :red;   :dmxOffset 1 ],
+            [ :outputAttr :green; :dmxOffset 2 ],
+            [ :outputAttr :blue;  :dmxOffset 3 ] .
+
+'''
+
+t0 = 0 # time
+
 class MockOutput(object):
-    def __init__(self, connections):
+    def __init__(self, uri, connections):
         self.connections = connections
         self.updates = []
+        self.uri = uri
+        self.numChannels = 4
 
     def allConnections(self):
         return self.connections
@@ -31,29 +53,30 @@
     def flush(self):
         self.updates.append('flush')
 
+@unittest.skip("outputMap got rewritten and mostly doesn't raise on these cases")
 class TestOutputMap(unittest.TestCase):
     def testWorking(self):
-        out0 = MockOutput([(0, DMX0['c1'])])
+        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
         m = outputMap(MockSyncedGraph(PREFIX + '''
           dmx0:c1 :connectedTo dev:inst1Brightness .
           dev:inst1 a :Device; :brightness dev:inst1Brightness .
         '''), [out0])
         self.assertEqual({(DEV['inst1'], L9['brightness']): (out0, 0)}, m)
+        
     def testMissingOutput(self):
-        out0 = MockOutput([(0, DMX0['c1'])])
+        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
         self.assertRaises(KeyError, outputMap, MockSyncedGraph(PREFIX + '''
-          dmx0:c2 :connectedTo dev:inst1Brightness .
           dev:inst1 a :Device; :brightness dev:inst1Brightness .
         '''), [out0])
 
     def testMissingOutputConnection(self):
-        out0 = MockOutput([(0, DMX0['c1'])])
+        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
         self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
           dev:inst1 a :Device; :brightness dev:inst1Brightness .
         '''), [out0])
 
     def testMultipleOutputConnections(self):
-        out0 = MockOutput([(0, DMX0['c1'])])
+        out0 = MockOutput(UDMX, [(0, DMX0['c1'])])
         self.assertRaises(ValueError, outputMap, MockSyncedGraph(PREFIX + '''
           dmx0:c1 :connectedTo dev:inst1Brightness .
           dmx0:c2 :connectedTo dev:inst1Brightness .
@@ -64,137 +87,147 @@
 
 class TestCollector(unittest.TestCase):
     def setUp(self):
-        self.config = MockSyncedGraph(PREFIX + '''
-
-        udmx:c1 :connectedTo dev:colorStripRed .
-        udmx:c2 :connectedTo dev:colorStripGreen .
-        udmx:c3 :connectedTo dev:colorStripBlue .
-        udmx:c4 :connectedTo dev:colorStripMode .
+        self.config = MockSyncedGraph(PREFIX + THEATER + '''
 
         dev:colorStrip a :Device, :ChauvetColorStrip;
+          :dmxUniverse udmx:; :dmxBase 1;
           :red dev:colorStripRed;
           :green dev:colorStripGreen;
           :blue dev:colorStripBlue;
           :mode dev:colorStripMode .
 
-        dmx0:c1 :connectedTo dev:inst1Brightness .
-        dev:inst1 a :Device, :Dimmer;
-          :brightness dev:inst1Brightness .
+        dev:inst1 a :Device, :SimpleDimmer;
+          :dmxUniverse dmx0:; :dmxBase 1;
+          :level dev:inst1Brightness .
         ''')
 
-        self.dmx0 = MockOutput([(0, DMX0['c1'])])
-        self.udmx = MockOutput([(0, UDMX['c1']),
-                                (1, UDMX['c2']),
-                                (2, UDMX['c3']),
-                                (3, UDMX['c4'])])
+        self.dmx0 = MockOutput(DMX0[None], [(0, DMX0['c1'])])
+        self.udmx = MockOutput(UDMX[None], [(0, UDMX['c1']),
+                                      (1, UDMX['c2']),
+                                      (2, UDMX['c3']),
+                                      (3, UDMX['c4'])])
 
     def testRoutesColorOutput(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
         c.setAttrs('client', 'sess1',
-                   [(DEV['colorStrip'], L9['color'], '#00ff00')])
+                   [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 
-        self.assertEqual([[0, 255, 0, 215], 'flush'], self.udmx.updates)
-        self.assertEqual([], self.dmx0.updates)
+        self.assertEqual([[215, 0, 255, 0], 'flush'], self.udmx.updates)
+        self.assertEqual([[0, 0, 0, 0], 'flush'], self.dmx0.updates)
 
     def testOutputMaxOfTwoClients(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
         c.setAttrs('client1', 'sess1',
-                   [(DEV['colorStrip'], L9['color'], '#ff0000')])
+                   [(DEV['colorStrip'], L9['color'], '#ff0000')], t0)
         c.setAttrs('client2', 'sess1',
-                   [(DEV['colorStrip'], L9['color'], '#333333')])
+                   [(DEV['colorStrip'], L9['color'], '#333333')], t0)
 
-        self.assertEqual([[255, 0, 0, 215], 'flush',
-                          [255, 51, 51, 215], 'flush'],
+        self.assertEqual([[215, 255, 0, 0], 'flush',
+                          [215, 255, 51, 51], 'flush'],
                          self.udmx.updates)
-        self.assertEqual([], self.dmx0.updates)
+        self.assertEqual([[0, 0, 0, 0], 'flush', [0, 0, 0, 0], 'flush'],
+                         self.dmx0.updates)
 
     def testClientOnSameOutputIsRememberedOverCalls(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
         c.setAttrs('client1', 'sess1',
-                   [(DEV['colorStrip'], L9['color'], '#080000')])
+                   [(DEV['colorStrip'], L9['color'], '#080000')], t0)
         c.setAttrs('client2', 'sess1',
-                   [(DEV['colorStrip'], L9['color'], '#060000')])
+                   [(DEV['colorStrip'], L9['color'], '#060000')], t0)
         c.setAttrs('client1', 'sess1',
-                   [(DEV['colorStrip'], L9['color'], '#050000')])
+                   [(DEV['colorStrip'], L9['color'], '#050000')], t0)
 
-        self.assertEqual([[8, 0, 0, 215], 'flush',
-                          [8, 0, 0, 215], 'flush',
-                          [6, 0, 0, 215], 'flush'],
+        self.assertEqual([[215, 8, 0, 0], 'flush',
+                          [215, 8, 0, 0], 'flush',
+                          [215, 6, 0, 0], 'flush'],
                          self.udmx.updates)
-        self.assertEqual([], self.dmx0.updates)
+        self.assertEqual([[0, 0, 0, 0], 'flush',
+                          [0, 0, 0, 0], 'flush',
+                          [0, 0, 0, 0], 'flush'],
+                         self.dmx0.updates)
 
     def testClientsOnDifferentOutputs(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
-        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#aa0000')])
-        c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
+        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#aa0000')], t0)
+        c.setAttrs('client2', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], t0)
 
         # ok that udmx is flushed twice- it can screen out its own duplicates
-        self.assertEqual([[170, 0, 0, 215], 'flush',
-                          [170, 0, 0, 215], 'flush'], self.udmx.updates)
-        self.assertEqual([[127], 'flush'], self.dmx0.updates)
+        self.assertEqual([[215, 170, 0, 0], 'flush',
+                          [215, 170, 0, 0], 'flush'], self.udmx.updates)
+        self.assertEqual([[0, 0, 0, 0], 'flush',
+                          [127, 0, 0, 0], 'flush'], self.dmx0.updates)
 
     def testNewSessionReplacesPreviousOutput(self):
         # ..as opposed to getting max'd with it
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
 
-        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)])
-        c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)])
+        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .8)], t0)
+        c.setAttrs('client1', 'sess2', [(DEV['inst1'], L9['brightness'], .5)], t0)
 
-        self.assertEqual([[204], 'flush', [127], 'flush'], self.dmx0.updates)
+        self.assertEqual([[204, 0, 0, 0], 'flush',
+                          [127, 0, 0, 0], 'flush'], self.dmx0.updates)
 
     def testNewSessionDropsPreviousSettingsOfOtherAttrs(self):
-        
-        c = Collector(MockSyncedGraph(PREFIX + '''
-
-        udmx:c1 :connectedTo dev:colorStripRed .
-        udmx:c2 :connectedTo dev:colorStripGreen .
-        udmx:c3 :connectedTo dev:colorStripBlue .
-        udmx:c4 :connectedTo dev:colorStripMode .
+        c = Collector(MockSyncedGraph(PREFIX + THEATER + '''
 
         dev:colorStrip a :Device, :ChauvetColorStrip;
+          :dmxUniverse udmx:; :dmxBase 1;
           :red dev:colorStripRed;
           :green dev:colorStripGreen;
           :blue dev:colorStripBlue;
           :mode dev:colorStripMode .
 
-        dmx0:c1 :connectedTo dev:inst1Brightness .
-        dev:inst1 a :Device, :Dimmer;
-          :brightness dev:inst1Brightness .
+        dev:inst1 a :Device, :SimpleDimmer;
+          :dmxUniverse dmx0:; :dmxBase 0;
+          :level dev:inst1Brightness .
         '''), outputs=[self.dmx0, self.udmx])
 
         c.setAttrs('client1', 'sess1',
-                   [(DEV['colorStrip'], L9['color'], '#ff0000')])
+                   [(DEV['colorStrip'], L9['color'], '#ff0000')], t0)
         c.setAttrs('client1', 'sess2',
-                   [(DEV['colorStrip'], L9['color'], '#00ff00')])
+                   [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 
-        self.assertEqual([[255, 0, 0, 215], 'flush',
-                          [0, 255, 0, 215], 'flush'], self.udmx.updates)
+        self.assertEqual([[215, 255, 0, 0], 'flush',
+                          [215, 0, 255, 0], 'flush'], self.udmx.updates)
 
     def testClientIsForgottenAfterAWhile(self):
         with freeze_time(datetime.datetime.now()) as ft:
             c = Collector(self.config, outputs=[self.dmx0, self.udmx])
-            c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
+            c.setAttrs('cli1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)],
+                       time.time())
             ft.tick(delta=datetime.timedelta(seconds=1))
-            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)])
+            # this max's with cli1's value so we still see .5
+            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .2)], 
+                       time.time())
             ft.tick(delta=datetime.timedelta(seconds=9.1))
-            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)])
-            self.assertEqual([[127], 'flush', [127], 'flush', [102], 'flush'],
+            # now cli1 is forgotten, so our value appears
+            c.setAttrs('cli2', 'sess1', [(DEV['inst1'], L9['brightness'], .4)], 
+                       time.time())
+            self.assertEqual([[127, 0, 0, 0], 'flush',
+                              [127, 0, 0, 0], 'flush',
+                              [102, 0, 0, 0], 'flush'],
                              self.dmx0.updates)
 
-    def testClientUpdatesAreCollected(self):
-        # second call to setAttrs doesn't forget the first
+    def testClientUpdatesAreNotMerged(self):
+        # second call to setAttrs forgets the first
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
+        t0 = time.time()
+        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)], t0)
+        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)], t0)
+        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#00ff00')], t0)
 
-        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], .5)])
-        c.setAttrs('client1', 'sess1', [(DEV['inst1'], L9['brightness'], 1)])
-        c.setAttrs('client1', 'sess1', [(DEV['colorStrip'], L9['color'], '#00ff00')])
-
-        self.assertEqual([[0, 255, 0, 215], 'flush'], self.udmx.updates)
-        self.assertEqual([[127], 'flush', [255], 'flush', [255], 'flush'], self.dmx0.updates)
+        self.assertEqual([[0, 0, 0, 0], 'flush',
+                          [0, 0, 0, 0], 'flush',
+                          [215, 0, 255, 0], 'flush'],
+                         self.udmx.updates)
+        self.assertEqual([[127, 0, 0, 0], 'flush',
+                          [255, 0, 0, 0], 'flush',
+                          [0, 0, 0, 0], 'flush'],
+                         self.dmx0.updates)
 
     def testRepeatedAttributesInOneRequestGetResolved(self):
         c = Collector(self.config, outputs=[self.dmx0, self.udmx])
@@ -202,12 +235,13 @@
         c.setAttrs('client1', 'sess1', [
             (DEV['inst1'], L9['brightness'], .5),
             (DEV['inst1'], L9['brightness'], .3),
-        ])
-        self.assertEqual([[127], 'flush'], self.dmx0.updates)
+        ], t0)
+        self.assertEqual([[127, 0, 0, 0], 'flush'], self.dmx0.updates)
 
         c.setAttrs('client1', 'sess1', [
             (DEV['inst1'], L9['brightness'], .3),
             (DEV['inst1'], L9['brightness'], .5),
-        ])
-        self.assertEqual([[127], 'flush', [127], 'flush'], self.dmx0.updates)
+        ], t0)
+        self.assertEqual([[127, 0, 0, 0], 'flush',
+                          [127, 0, 0, 0], 'flush'], self.dmx0.updates)
 
--- a/light9/collector/device.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/collector/device.py	Tue Mar 28 07:48:10 2017 +0000
@@ -76,7 +76,7 @@
         out = deviceAttrSettings.get(attr)
         if out is None:
             return default
-        return float(out.toPython())
+        return float(out.toPython()) if isinstance(out, Literal) else out
 
     def rgbAttr(attr):
         color = deviceAttrSettings.get(attr, '#000000')
--- a/light9/collector/device_test.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/collector/device_test.py	Tue Mar 28 07:48:10 2017 +0000
@@ -20,8 +20,8 @@
         
 class TestDimmer(unittest.TestCase):
     def testConvert(self):
-        self.assertEqual({L9['brightness']: 127},
-                         toOutputAttrs(L9['Dimmer'], {L9['brightness']: .5}))
+        self.assertEqual({L9['level']: 127},
+                         toOutputAttrs(L9['SimpleDimmer'], {L9['brightness']: .5}))
 
 class TestMini15(unittest.TestCase):
     def testConvertColor(self):
--- a/light9/collector/output.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/collector/output.py	Tue Mar 28 07:48:10 2017 +0000
@@ -23,6 +23,8 @@
     often as you want- the result will be sent as soon as possible,
     and with repeats as needed to outlast hardware timeouts.
     """
+    uri = None  # type: URIRef
+    numChannels = None  # type: int
     def __init__(self):
         raise NotImplementedError
         
--- a/light9/collector/output_test.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/collector/output_test.py	Tue Mar 28 07:48:10 2017 +0000
@@ -33,14 +33,6 @@
         self.assertEqual([0, 1, 1], x)
         
 class TestDmxOutput(unittest.TestCase):
-    def testGeneratesConnectionList(self):
-        out = DmxOutput(L9['output/udmx/'], 3)
-        self.assertEqual([
-            (0, L9['output/udmx/c1']),
-            (1, L9['output/udmx/c2']),
-            (2, L9['output/udmx/c3']),
-        ], list(out.allConnections()))
-
     def testFlushIsNoop(self):
         out = DmxOutput(L9['output/udmx/'], 3)
         out.flush()
--- a/light9/io/__init__.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/io/__init__.py	Tue Mar 28 07:48:10 2017 +0000
@@ -1,7 +1,7 @@
 from __future__ import division
 import sys
 
-class BaseIO:
+class BaseIO(object):
     def __init__(self):
         self.dummy=1
         self.__name__ = 'BaseIO'
@@ -83,57 +83,3 @@
                                   for lev in levels]) + "\x55"
         self._dmx().write(packet)
         
-class SerialPots(BaseIO):
-    """
-    this is a dummy object (that returns zeros forever) until you call startup()
-    which makes it bind to the port, etc
-    
-    """
-    def __init__(self):
-        # no init here- call getport() to actually initialize
-        self.dummy=1
-        self.__name__='SerialPots' # i thought this was automatic!
-
-    def golive(self):
-        """
-        ls -l /dev/i2c-0
-        crw-rw-rw-    1 root     root      89,   0 Jul 11 12:27 /dev/i2c-0
-        """
-        import serport
-        self.serport = serport
-        
-        self.f = open("/dev/i2c-0","rw")
-
-        # this is for a chip with A0,A1,A2 lines all low:
-        port = 72
-
-        from fcntl import *
-
-        I2C_SLAVE = 0x0703  #/* Change slave address                 */
-        ioctl(self.f,I2C_SLAVE,port)
-        self.dummy=0
-
-    def godummy(self):
-        BaseIO.godummy(self)
-        self.f.close()
-
-    def getlevels(self):
-        if self.dummy:
-            return (0,0,0,0)
-        else:
-            return self.serport.read_all_adc(self.f.fileno())
-
-
-if __name__=='__main__':
-    
-    """ tester program that just dumps levels for a while """
-    from time import sleep
-    from serport import *
-
-    i=0
-    while i<100:
-        sleep(.033)
-        i=i+1
-
-        print read_all_adc(f.fileno())
-
--- a/light9/rdfdb/graphfile_test.py	Tue Mar 28 05:04:07 2017 +0000
+++ b/light9/rdfdb/graphfile_test.py	Tue Mar 28 07:48:10 2017 +0000
@@ -27,6 +27,8 @@
         gf.flush()
         wroteContent = open(tf.name).read()
         self.assertEqual('''@prefix : <http://example.com/> .
+@prefix dev: <http://light9.bigasterisk.com/device/> .
+@prefix effect: <http://light9.bigasterisk.com/effect/> .
 @prefix n: <http://example.com/n/> .
 @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
 @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@@ -34,6 +36,4 @@
 @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
 
 :boo n:two <http://example.com/other/ns> .
-
 ''', wroteContent)
-
--- a/makefile	Tue Mar 28 05:04:07 2017 +0000
+++ b/makefile	Tue Mar 28 07:48:10 2017 +0000
@@ -102,3 +102,11 @@
 
 coffee:
 	zsh -c 'coffee -cw light9/web/{.,live,timeline}/*.coffee'
+
+env-mypy/bin/mypy:
+	mkdir -p env-mypy
+	virtualenv -p /usr/bin/python3  env-mypy/
+	env-mypy/bin/pip install mypy==0.501 lxml==3.7.3
+
+mypy-collector: env-mypy/bin/mypy
+	env-mypy/bin/mypy --py2 --ignore-missing-imports --strict-optional --custom-typeshed-dir stubs --html-report /tmp/rep bin/collector light9/collector/*.py
--- a/requirements.txt	Tue Mar 28 05:04:07 2017 +0000
+++ b/requirements.txt	Tue Mar 28 07:48:10 2017 +0000
@@ -34,3 +34,5 @@
 git+http://github.com/drewp/scales.git@448d59fb491b7631877528e7695a93553bfaaa93#egg=scales
 colormath==2.1.1
 noise==1.2.2
+
+typing==3.6.1