changeset 233:4ebb5cc30002

server/browser graph sync. cut dependency on the WS version. merge some changes between arduino/pi code. Ignore-this: cf7d20d54e134e8ff33a9ee405610846
author drewp@bigasterisk.com
date Sat, 30 Jan 2016 06:40:00 -0800
parents 4e91f3ec460b
children 5bbcf7d9a5f5
files lib/patchablegraph.py service/arduinoNode/arduinoNode.py service/arduinoNode/devices.py service/arduinoNode/static/index.html service/arduinoNode/static/output-widgets.html service/piNode/config.n3 service/piNode/config/main.n3 service/piNode/devices.py service/piNode/piNode.py
diffstat 9 files changed, 293 insertions(+), 196 deletions(-) [+]
line wrap: on
line diff
--- a/lib/patchablegraph.py	Thu Jan 28 02:48:54 2016 -0800
+++ b/lib/patchablegraph.py	Sat Jan 30 06:40:00 2016 -0800
@@ -10,8 +10,17 @@
 5. Client queries its graph with low-level APIs or client-side sparql.
 6. When the graph changes, the client knows and can update itself at
    low or high granularity.
+
+
+See also:
+* http://iswc2007.semanticweb.org/papers/533.pdf RDFSync: efficient remote synchronization of RDF
+models
+* https://www.w3.org/2009/12/rdf-ws/papers/ws07 Supporting Change Propagation in RDF
+* https://www.w3.org/DesignIssues/lncs04/Diff.pdf Delta: an ontology for the distribution of
+differences between RDF graphs
+
 """
-import sys, json
+import sys, json, logging
 import cyclone.sse
 sys.path.append("/my/proj/light9")
 from light9.rdfdb.grapheditapi import GraphEditApi
@@ -20,6 +29,8 @@
 from rdflib_jsonld.serializer import from_rdf
 from cycloneerr import PrettyErrorHandler
 
+log = logging.getLogger('patchablegraph')
+
 def writeGraphResponse(req, graph, acceptHeader):
     if acceptHeader == 'application/nquads':
         req.set_header('Content-type', 'application/nquads')
@@ -46,6 +57,11 @@
         'deletes': from_rdf(_graphFromQuads2(p.delQuads)),
     }})
 
+def graphAsJson(g):
+    # This is not the same as g.serialize(format='json-ld')! That
+    # version omits literal datatypes.
+    return json.dumps(from_rdf(g))
+    
 class PatchableGraph(GraphEditApi):
     """
     Master graph that you modify with self.patch, and we get the
@@ -68,6 +84,9 @@
         for ob in self._observers:
             ob(patchAsJson(p))
 
+    def asJsonLd(self):
+        return graphAsJson(self._graph)
+            
     def addObserver(self, onPatch):
         self._observers.append(onPatch)
         
@@ -102,10 +121,9 @@
         self.masterGraph = masterGraph
         
     def bind(self):
-        self.sendEvent(
-            message=self.masterGraph.serialize(None, format='json-ld',
-                                               indent=None),
-            event='fullGraph')
+        graphJson = self.masterGraph.asJsonLd()
+        log.debug("send fullGraph event: %s", graphJson)
+        self.sendEvent(message=graphJson, event='fullGraph')
         self.masterGraph.addObserver(self.onPatch)
 
     def onPatch(self, patchJson):
--- a/service/arduinoNode/arduinoNode.py	Thu Jan 28 02:48:54 2016 -0800
+++ b/service/arduinoNode/arduinoNode.py	Sat Jan 30 06:40:00 2016 -0800
@@ -41,7 +41,8 @@
 
 ACTION_BASE = 10 # higher than any of the fixed command numbers
 
-CTX = ROOM['arduinosOn%s' % socket.gethostname()]
+hostname = socket.gethostname()
+CTX = ROOM['arduinosOn%s' % hostname]
 
 class Config(object):
     def __init__(self, masterGraph):
@@ -74,10 +75,8 @@
         self.masterGraph = masterGraph
         self.dev = dev
 
-        self.masterGraph.patch(Patch(addQuads=[
-            (HOST[socket.gethostname()], ROOM['connectedTo'], self.uri, CTX),
-        ]))
-        
+        self.masterGraph.patch(Patch(addQuads=self.staticStmts()))
+
         # The order of this list needs to be consistent between the
         # deployToArduino call and the poll call.
         self._devs = devices.makeDevices(configGraph, self.uri)
@@ -98,7 +97,6 @@
             'dev': self.dev,
             'baudrate': self.baudrate,
             'devices': [d.description() for d in self._devs],
-            'graph': 'http://%s6:9059/graph' % socket.gethostname(), #todo
             }
         
     def open(self):
@@ -143,14 +141,21 @@
     def _exportToGraphite(self):
         # note this is writing way too often- graphite is storing at a lower res
         now = time.time()
+        # 20 sec is not precise; just trying to reduce wifi traffic
+        if getattr(self, 'lastGraphiteExport', 0) + 20 > now:
+            return
+        self.lastGraphiteExport = now
+        log.debug('graphite export:')
         # objects of these statements are suitable as graphite values.
-        graphitePredicates = {ROOM['temperatureF']} 
+        graphitePredicates = {ROOM['temperatureF']}
+        # bug: one sensor can have temp and humid- this will be ambiguous
         for s, graphiteName in self.configGraph.subject_objects(ROOM['graphiteName']):
             for group in self._statementsFromInputs.values():
                 for stmt in group:
                     if stmt[0] == s and stmt[1] in graphitePredicates:
+                        log.debug('  sending %s -> %s', stmt[0], graphiteName)
                         self._carbon.send(graphiteName, stmt[2].toPython(), now)
-        
+
     def outputStatements(self, stmts):
         unused = set(stmts)
         for dev in self._devs:
@@ -176,19 +181,24 @@
                 # should be good enough. The right answer is to give
                 # each dev the masterGraph for it to write to.
                 self.syncMasterGraphToHostStatements(dev)
-                log.info("success")
+                log.info("output and masterGraph sync complete")
         if unused:
             log.info("Board %s doesn't care about these statements:", self.uri)
             for s in unused:
-                log.info("%r", s)
+                log.warn("%r", s)
 
     def syncMasterGraphToHostStatements(self, dev):
         hostStmtCtx = URIRef(dev.uri + '/host')
         newQuads = inContext(dev.hostStatements(), hostStmtCtx)
-        self.masterGraph.patchSubgraph(hostStmtCtx, newQuads)
-                
+        p = self.masterGraph.patchSubgraph(hostStmtCtx, newQuads)
+        log.debug("patch master with these host stmts %s", p)
+
+    def staticStmts(self):
+        return [(HOST[hostname], ROOM['connectedTo'], self.uri, CTX)]
+
     def generateArduinoCode(self):
-        code = write_arduino_code.writeCode(self.baudrate, self._devs, self._devCommandNum)
+        code = write_arduino_code.writeCode(self.baudrate, self._devs,
+                                            self._devCommandNum)
         code = write_arduino_code.indent(code)
         cksum = hashlib.sha1(code).hexdigest()
         code = code.replace('CODE_CHECKSUM', cksum)
@@ -300,10 +310,9 @@
         
 class Boards(cyclone.web.RequestHandler):
     def get(self):
-        
         self.set_header('Content-type', 'application/json')
         self.write(json.dumps({
-            'host': socket.gethostname(),
+            'host': hostname,
             'boards': [b.description() for b in self.settings.boards]
         }, indent=2))
             
@@ -344,7 +353,7 @@
         b.startPolling()
 
 
-    app = cyclone.web.Application([
+    reactor.listenTCP(9059, cyclone.web.Application([
         (r"/()", cyclone.web.StaticFileHandler, {
             "path": "static", "default_filename": "index.html"}),
         (r'/static/(.*)', cyclone.web.StaticFileHandler, {"path": "static"}),
@@ -354,8 +363,7 @@
         (r'/output', OutputPage),
         (r'/arduinoCode', ArduinoCode),
         (r'/dot', Dot),
-        ], config=config, boards=boards)
-    reactor.listenTCP(9059, app, interface='::')
+        ], config=config, boards=boards), interface='::')
     reactor.run()
 
 main()
--- a/service/arduinoNode/devices.py	Thu Jan 28 02:48:54 2016 -0800
+++ b/service/arduinoNode/devices.py	Sat Jan 30 06:40:00 2016 -0800
@@ -51,7 +51,7 @@
         return {
             'uri': self.uri,
             'className': self.__class__.__name__,
-            'pinNumber': self.pinNumber,
+            'pinNumber': getattr(self, 'pinNumber', None),
             'outputPatterns': self.outputPatterns(),
             'watchPrefixes': self.watchPrefixes(),
             'outputWidgets': self.outputWidgets(),
--- a/service/arduinoNode/static/index.html	Thu Jan 28 02:48:54 2016 -0800
+++ b/service/arduinoNode/static/index.html	Sat Jan 30 06:40:00 2016 -0800
@@ -1,7 +1,7 @@
 <!doctype html>
 <html>
   <head>
-    <title>arduinoNode - </title>
+    <title>ha - </title>
     <meta name=viewport content="width=device-width, initial-scale=1">
     <meta charset="utf-8" />
     <script src="/lib/polymer/1.0.9/webcomponentsjs/webcomponents.min.js"></script>
@@ -9,12 +9,11 @@
     <link rel="import" href="/lib/polymer/1.0.9/iron-ajax/iron-ajax.html">
     <link rel="import" href="/lib/polymer/1.0.9/iron-flex-layout/iron-flex-layout.html">
     <link rel="import" href="/lib/polymer/1.0.9/paper-button/paper-button.html">
-    <link rel="import" href="/room/ari/static/rdf-observe.html">
-    <link rel="import" href="/room/ari/static/rdf-oneshot.html">
-    <link rel="import" href="/room/ari/static/rdf-uri.html">
-    <link rel="import" href="static/output-widgets.html">
+    <link rel="import" href="/rdf/rdf-oneshot.html">
+    <link rel="import" href="/rdf/rdf-uri.html">
     <link rel="import" href="/rdf/streamed-graph.html">
     <link rel="import" href="/rdf/graph-view.html">
+    <link rel="import" href="static/output-widgets.html">
     <style>
      body {
        font-family: monospace;
@@ -51,15 +50,11 @@
       </script>
     </dom-module>
 
-    <!-- subj/pred are compact uris -->
     <dom-module id="watched-subgraph">
       <style>
        .read { font-weight: bold; }
       </style>
       <template>
-        <rdf-observe graph="{{graph}}" subject="{{subj}}" predicate="{{pred}}" out="{{out}}">
-        </rdf-observe>
-
         <div><span>{{compactUri(subj)}}</span>, <span>{{compactUri(pred)}}</span>,
           <span class="read">{{formatted(out)}}</span>
         </div>
@@ -70,18 +65,28 @@
          Polymer({
            is: 'watched-subgraph',
            properties: {
-             graph: { notify: true },
+             streamedGraph: { notify: true, observer: 'onGraphChange' }, // streamed-graph output
              out: { notify: true },
-             pred: { notify: true },
-             subj: { notify: true }
+             pred: { notify: true }, // uri
+             subj: { notify: true } // uri
            },
            behaviors: [BigastUri],
-           formatted: function (out) {
-             var obj = out[this.compactUri(this.pred)];
-             if (typeof obj == 'string') {
+           onGraphChange: function(streamedGraph) {
+             if (!streamedGraph.graph) {
+               return;
+             }
+             var env = streamedGraph.graph.store.rdf;
+             streamedGraph.graph.quadStore.quads({subject: env.createNamedNode(this.subj),
+                                                  predicate: env.createNamedNode(this.pred)},
+                                                 function(quad) {
+               this.out = quad.object.valueOf();
+             }.bind(this));
+           },
+           formatted: function (obj) {
+             if (typeof(obj) == 'string') {
                return this.compactUri(obj);
              } else {
-               return obj['@value'];
+               return obj;
              }
            }
          });
@@ -111,15 +116,22 @@
        .device {
          background: #fff;
        }
+       .devs {
+         -webkit-column-width: 440px;
+       }
+       .devs .device {
+         -webkit-column-break-inside: avoid;
+       }
       </style>
       <template>
+        <streamed-graph url="graph/events" graph="{{graph}}"></streamed-graph>
         <iron-ajax url="boards" auto="true" handle-as="json"
                    last-response="{{ret}}"></iron-ajax>
         <template is="dom-repeat" items="{{ret.boards}}" as="board">
           <div class="board">
             <h1>Board <linked-uri href$="{{board.uri}}"></linked-uri></h1>
             <h2>Devices</h2>
-            <ul>
+            <ul class="devs">
               <template is="dom-repeat" items="{{board.devices}}" as="dev">
                 <div class="device">
                   <h1>
@@ -130,7 +142,7 @@
                     <div>watching:</div>
                     <ul>
                       <template is="dom-repeat" items="{{dev.watchPrefixes}}" as="prefix">
-                        <watched-subgraph graph="{{board.graph}}" subj="{{prefix.0}}" pred="{{prefix.1}}"></watched-subgraph>
+                        <watched-subgraph streamed-graph="{{graph}}" subj="{{prefix.0}}" pred="{{prefix.1}}"></watched-subgraph>
                       </template>
                     </ul>
                   </template>
@@ -140,7 +152,7 @@
                     <ul>
                       <template is="dom-repeat" items="{{dev.outputWidgets}}" as="out">
                         <div>
-                          <output-widget-any desc="{{out}}"></output-widget-any>
+                          <output-widget-any streamed-graph="{{graph}}" desc="{{out}}"></output-widget-any>
                         </div>
                       </template>
                     </ul>
@@ -150,7 +162,6 @@
             </ul>
           </div>
         </template>
-        <streamed-graph url="graph/events" graph="{{graph}}"></streamed-graph>
         <graph-view graph="{{graph}}"></graph-view>
       </template>
       <script>
--- a/service/arduinoNode/static/output-widgets.html	Thu Jan 28 02:48:54 2016 -0800
+++ b/service/arduinoNode/static/output-widgets.html	Sat Jan 30 06:40:00 2016 -0800
@@ -5,7 +5,6 @@
 
 <dom-module id="output-sender">
   <template>
-    <iron-ajax id="graphGet" url="../graph" method="GET" headers='{"Accept": "application/ld+json"}'></iron-ajax>
     <iron-ajax id="output" url="../output" method="PUT"></iron-ajax>
     Set <a href$="{{subj}}">{{compactUri(subj)}}</a>'s
     <span>{{compactUri(pred)}}</span> to
@@ -15,41 +14,34 @@
      is: 'output-sender',
      behaviors: [BigastUri],
      properties: {
+       streamedGraph: { notify: true, observer: 'onGraphChange' },
        subj: { notify: true },
        pred: { notify: true },
-       value: { notify: true, observer: 'valueChanged' }
+       value: { notify: true, observer: 'browserChangedValue' }
      },
      ready: function() {
        this.waitOnChangeMs = 100;
-       this.smallestRequestPeriodMs = 200;
+       this.smallestRequestPeriodMs = 100;
        this.synced = false;
        
        this.newRequestNeedsSending = false;
        this.lastSendMs = 0;
        this.$.output.addEventListener('response', this.onResponse.bind(this));
 
-       this.loadInitialValue();
      },
-     loadInitialValue: function() {
-       this.$.graphGet.addEventListener('response', function(ev) {
-         ev.target.removeEventListener(ev.type, arguments.callee);
-
-         ev.detail.response.forEach(function(row) {
-           var subj = row['@id'];
-           if (subj == this.subj) {
-             Object.keys(row).forEach(function(pred) {
-               if (pred == this.pred) {
-                 row[pred].forEach(function(obj) {
-                   this.value = obj['@value'];
-                   this.synced = true;
-                 }.bind(this));
-               }
-             }.bind(this));
-           }
-         }.bind(this));
-
+     onGraphChange: function(streamedGraph) {
+       if (!streamedGraph.graph) {
+         return;
+       }
+       console.log('output-sender sees change to version', streamedGraph.version);
+       console.log('the index im about to read:', stringifyMap(streamedGraph.graph.quadStore.index));
+       var env = streamedGraph.graph.store.rdf;
+       streamedGraph.graph.quadStore.quads({
+         subject: env.createNamedNode(this.subj),
+         predicate: env.createNamedNode(this.pred)
+       }, function(quad) {
+         this.serverChangedValue(quad.object.valueOf());
        }.bind(this));
-       this.$.graphGet.generateRequest();
      },
      onResponse: function() {
        if (!this.newRequestNeedsSending) {
@@ -67,9 +59,10 @@
        }
        this.newRequestNeedsSending = false;
        this.lastSendMs = now;
+       console.log(Date.now(), 'sending', this.$.output.body);
        this.$.output.generateRequest();
      },
-     valueChanged: function () {
+     browserChangedValue: function () {
        if (!this.subj || !this.pred) {
          return;
        }
@@ -78,6 +71,11 @@
        this.$.output.body = this.value;
        this.newRequestNeedsSending = true;
        setTimeout(this.onResponse.bind(this), this.waitOnChangeMs);
+     },
+     serverChangedValue: function(v) {
+       console.log('server gave', v);
+       this.value = v;
+       this.synced = true;
      }
    });
   </script>
@@ -87,7 +85,7 @@
   <template>
     <div style="display: flex">
       <div>
-        <output-sender subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
+        <output-sender streamed-graph="{{streamedGraph}}" subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
         <div>color pick <span>{{value}}</span>
         <button on-click="black">Black</button>
         <button on-click="white">White</button>
@@ -117,18 +115,19 @@
 
 <dom-module id="output-slider">
   <template>
-    <output-sender subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
+    <output-sender streamed-graph="{{streamedGraph}}" subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
     <input type="range" min="{{min}}" max="{{max}}" step="{{step}}" value="{{value::input}}"> <span>{{value}}</span>
   </template>
   <script>
-    Polymer({
-      is: 'output-slider',
-      properties: {
-        max: { notify: true },
-        min: { notify: true },
-        step: { notify: true }
-      },
-    });
+   Polymer({
+     is: 'output-slider',
+     properties: {
+       streamedGraph: { notify: true }, 
+       max: { notify: true },
+       min: { notify: true },
+       step: { notify: true }
+     },
+   });
   </script>
 </dom-module>
 
@@ -138,7 +137,7 @@
  -->
 <dom-module id="output-fixed-text">
   <template>
-    <output-sender subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
+    <output-sender streamed-graph="{{streamedGraph}}" subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
     <textarea rows="{{rows}}" cols="{{cols}}" value="{{value::input}}"></textarea>
   </template>
   <script>
@@ -154,7 +153,7 @@
 
 <dom-module id="output-switch">
   <template>
-    <output-sender subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
+    <output-sender streamed-graph="{{streamedGraph}}" subj="{{subj}}" pred="{{pred}}" value="{{value}}"></output-sender>
     <input type="checkbox" checked="{{check::change}}"> <span>{{value}}</span>
   </template>
   <script>
@@ -181,13 +180,20 @@
    Polymer({
      is: 'output-widget-any',
      properties: {
-       desc: { type: Object, notify: true }
+       desc: { type: Object, notify: true },
+       streamedGraph: { type: Object, notify: true, observer: 'onGraph' },
      },
      ready: function () {
-       var elem = document.createElement(this.desc.element);
-       this.appendChild(elem);
+       this.elem = document.createElement(this.desc.element);
+       this.appendChild(this.elem);
        for (var k of Object.keys(this.desc)) {
-         elem.setAttribute(k, this.desc[k]);
+         this.elem.setAttribute(k, this.desc[k]);
+       }
+       this.elem.streamedGraph = this.streamedGraph;
+     },
+     onGraph: function(g) {
+       if (this.elem) {
+         this.elem.streamedGraph = g;
        }
      }
    });
--- a/service/piNode/config.n3	Thu Jan 28 02:48:54 2016 -0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-@prefix rdfs:     <http://www.w3.org/2000/01/rdf-schema#> .
-@prefix :         <http://projects.bigasterisk.com/room/> .
-@prefix ha:       <http://bigasterisk.com/homeauto/> .
-@prefix sensor:   <http://bigasterisk.com/homeauto/sensor/> .
-@prefix houseLoc: <http://bigasterisk.com/homeauto/houseLoc/> .
-
-@prefix board2pin: <http://bigasterisk.com/homeauto/board2/pin/> .
-@prefix board2ow: <http://bigasterisk.com/homeauto/board2/oneWire/> .
-
-ha:node2 a :PiBoard;
-  :hostname "sticker";
-  :onboardDevice ha:bedroomPiTemp;
-  :hasPin
-    board2pin:GPIO2,
-    board2pin:GPIO3,
-    board2pin:GPIO4,
-    board2pin:GPIO17,
-    board2pin:GPIO27
-    .
-
-ha:bedroomPiTemp a :OnboardTemperature .
-
-board2pin:GPIO2 :gpioNumber 2 .
-board2pin:GPIO3 :gpioNumber 3 .
-board2pin:GPIO4 :gpioNumber 4 .
-board2pin:GPIO17 :gpioNumber 17 .
-board2pin:GPIO27 :gpioNumber 27 .
-
-board2pin:GPIO17 :connectedTo sensor:motion1 .
-sensor:motion1 a :MotionSensor;
-  :sees houseLoc:bed .
-
-:bedLedStrip a :RgbStrip;
-  :redChannel board2pin:GPIO2;
-  :greenChannel board2pin:GPIO3;
-  :blueChannel board2pin:GPIO4 .
-    
-board2pin:GPIO27 :connectedTo :headboardWhite .
-:headboardWhite a :LedOutput .
-  
-#board2pin:b29 :connectedTo board2ow: .
-#board2ow: a :OneWire;
-#  :connectedTo board2ow:temperatureSensor .
-#board2ow:temperatureSensor a :TemperatureSensor;
-#  :position :bed .
-
-@prefix board3pin: <http://bigasterisk.com/homeauto/board3/pin/> .
-@prefix board3ow: <http://bigasterisk.com/homeauto/board3/oneWire/> .
-
-ha:node3 a :PiBoard;
-  :hostname "kitchen";
-  :onboardDevice ha:kitchenPiTemp;
-  :hasPin
-    board3pin:GPIO4,
-    board3pin:GPIO17
-    .
-    
-ha:kitchenPiTemp a :OnboardTemperature .
-
-board3pin:GPIO4 :gpioNumber 4; :connectedTo sensor:tempHumidKitchen .
-sensor:tempHumidKitchen a :TempHumidSensor;
-  :sees houseLoc:kitchenCounter;
-  :graphiteName "system.house.temp.kitchenCounter" .
-
-board3pin:GPIO17 :gpioNumber 17; :connectedTo board3ow: .
-board3ow: a :OneWire; :connectedTo board3ow:dev-000003a5a94c .
-board3ow:dev-000003a5a94c a :TemperatureSensor;
-  :position houseLoc:kitchenCounter;
-  :graphiteName "system.house.temp.kitchenCounter_ds_test" .
-
-ha:node4 a :PiBoard;
-  :hostname "living";
-  :onboardDevice ha:livingPiTemp .
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/service/piNode/config/main.n3	Sat Jan 30 06:40:00 2016 -0800
@@ -0,0 +1,73 @@
+@prefix rdfs:     <http://www.w3.org/2000/01/rdf-schema#> .
+@prefix :         <http://projects.bigasterisk.com/room/> .
+@prefix ha:       <http://bigasterisk.com/homeauto/> .
+@prefix sensor:   <http://bigasterisk.com/homeauto/sensor/> .
+@prefix houseLoc: <http://bigasterisk.com/homeauto/houseLoc/> .
+
+@prefix board2pin: <http://bigasterisk.com/homeauto/board2/pin/> .
+@prefix board2ow: <http://bigasterisk.com/homeauto/board2/oneWire/> .
+
+ha:node2 a :PiBoard;
+  :hostname "sticker";
+  :onboardDevice ha:bedroomPiTemp;
+  :hasPin
+    board2pin:GPIO2,
+    board2pin:GPIO3,
+    board2pin:GPIO4,
+    board2pin:GPIO17,
+    board2pin:GPIO27
+    .
+
+ha:bedroomPiTemp a :OnboardTemperature .
+
+board2pin:GPIO2 :gpioNumber 2 .
+board2pin:GPIO3 :gpioNumber 3 .
+board2pin:GPIO4 :gpioNumber 4 .
+board2pin:GPIO17 :gpioNumber 17 .
+board2pin:GPIO27 :gpioNumber 27 .
+
+board2pin:GPIO17 :connectedTo sensor:motion1 .
+sensor:motion1 a :MotionSensor;
+  :sees houseLoc:bed .
+
+:bedLedStrip a :RgbStrip;
+  :redChannel board2pin:GPIO2;
+  :greenChannel board2pin:GPIO3;
+  :blueChannel board2pin:GPIO4 .
+    
+board2pin:GPIO27 :connectedTo :headboardWhite .
+:headboardWhite a :LedOutput .
+  
+#board2pin:b29 :connectedTo board2ow: .
+#board2ow: a :OneWire;
+#  :connectedTo board2ow:temperatureSensor .
+#board2ow:temperatureSensor a :TemperatureSensor;
+#  :position :bed .
+
+@prefix board3pin: <http://bigasterisk.com/homeauto/board3/pin/> .
+@prefix board3ow: <http://bigasterisk.com/homeauto/board3/oneWire/> .
+
+ha:node3 a :PiBoard;
+  :hostname "kitchen";
+  :onboardDevice ha:kitchenPiTemp;
+  :hasPin
+    board3pin:GPIO4,
+    board3pin:GPIO17
+    .
+    
+ha:kitchenPiTemp a :OnboardTemperature .
+
+board3pin:GPIO4 :gpioNumber 4; :connectedTo sensor:tempHumidKitchen .
+sensor:tempHumidKitchen a :TempHumidSensor;
+  :sees houseLoc:kitchenCounter;
+  :graphiteName "system.house.temp.kitchenCounter" .
+
+board3pin:GPIO17 :gpioNumber 17; :connectedTo board3ow: .
+board3ow: a :OneWire; :connectedTo board3ow:dev-000003a5a94c .
+board3ow:dev-000003a5a94c a :TemperatureSensor;
+  :position houseLoc:kitchenCounter;
+  :graphiteName "system.house.temp.kitchenCounter_ds_test" .
+
+ha:node4 a :PiBoard;
+  :hostname "living";
+  :onboardDevice ha:livingPiTemp .
\ No newline at end of file
--- a/service/piNode/devices.py	Thu Jan 28 02:48:54 2016 -0800
+++ b/service/piNode/devices.py	Sat Jan 30 06:40:00 2016 -0800
@@ -40,7 +40,14 @@
     def __init__(self, graph, uri, pi, pinNumber):
         self.graph, self.uri, self.pi = graph, uri, pi
         self.pinNumber = pinNumber
+        self.hostStateInit()
 
+    def hostStateInit(self):
+        """
+        If you don't want to use __init__, you can use this to set up
+        whatever storage you might need for hostStatements
+        """
+        
     def description(self):
         return {
             'uri': self.uri,
@@ -51,6 +58,16 @@
             'outputWidgets': self.outputWidgets(),
         }
 
+    def hostStatements(self):
+        """
+        Like readFromPoll but these statements come from the host-side
+        python code, not the connected device. Include output state
+        (e.g. light brightness) if its master version is in this
+        object. This method is called on /graph requests so it should
+        be fast.
+        """
+        return []
+        
     def watchPrefixes(self):
         """
         subj,pred pairs of the statements that might be returned from
@@ -159,13 +176,17 @@
     def __init__(self, graph, uri, pi, r, g, b):
         self.graph, self.uri, self.pi = graph, uri, pi
         self.rgb = map(int, [r, g, b])
+        self.value = '#000000'
             
     def setup(self):
         for i in self.rgb:
             self.pi.set_mode(i, pigpio.OUTPUT)
             self.pi.set_PWM_frequency(i, 200)
             self.pi.set_PWM_dutycycle(i, 0)
-
+            
+    def hostStatements(self):
+        return [(self.uri, ROOM['color'], Literal(self.value))]
+        
     def outputPatterns(self):
         return [(self.uri, ROOM['color'], None)]
 
@@ -178,6 +199,7 @@
         assert statements[0][:2] == (self.uri, ROOM['color'])
 
         rgb = self._rgbFromHex(statements[0][2])
+        self.value = statements[0][2]
 
         for (i, v) in zip(self.rgb, rgb):
             self.pi.set_PWM_dutycycle(i, v)
@@ -271,7 +293,10 @@
 @register
 class LedOutput(DeviceType):
     deviceType = ROOM['LedOutput']
-                
+
+    def hostStateInit(self):
+        self.value = 0
+    
     def setup(self):
         self.pi.set_mode(self.pinNumber, pigpio.OUTPUT)
         self.pi.set_PWM_frequency(self.pinNumber, 200)
@@ -283,8 +308,12 @@
     def sendOutput(self, statements):
         assert len(statements) == 1
         assert statements[0][:2] == (self.uri, ROOM['brightness'])
-        v = int(float(statements[0][2]) * 255)
+        self.value = float(statements[0][2])
+        v = int(self.value * 255)
         self.pi.set_PWM_dutycycle(self.pinNumber, v)
+
+    def hostStatements(self):
+        return [(self.uri, ROOM['brightness'], Literal(self.value))]       
         
     def outputWidgets(self):
         return [{
--- a/service/piNode/piNode.py	Thu Jan 28 02:48:54 2016 -0800
+++ b/service/piNode/piNode.py	Sat Jan 30 06:40:00 2016 -0800
@@ -1,10 +1,11 @@
 from __future__ import division
-import sys, logging, socket, json, time
+import sys, logging, socket, json, time, os
 import cyclone.web
 from rdflib import Namespace, URIRef, Literal, Graph, RDF, ConjunctiveGraph
 from rdflib.parser import StringInputSource
 from twisted.internet import reactor, task
 from docopt import docopt
+
 logging.basicConfig(level=logging.DEBUG)
 sys.path.append("/opt/homeauto_lib")
 from patchablegraph import PatchableGraph, CycloneGraphHandler, CycloneGraphEventsHandler
@@ -26,25 +27,30 @@
 
 log = logging.getLogger()
 logging.getLogger('serial').setLevel(logging.WARN)
+
 ROOM = Namespace('http://projects.bigasterisk.com/room/')
 HOST = Namespace('http://bigasterisk.com/ruler/host/')
 
 hostname = socket.gethostname()
 
+CTX = ROOM['pi/%s' % hostname]
+
 class Config(object):
     def __init__(self, masterGraph):
         self.graph = ConjunctiveGraph()
         log.info('read config')
-        self.graph.parse('config.n3', format='n3')
-        self.graph.bind('', ROOM) # maybe working
+        for f in os.listdir('config'):
+            if f.startswith('.'): continue
+            self.graph.parse('config/%s' % f, format='n3')
+        self.graph.bind('', ROOM) # not working
         self.graph.bind('rdf', RDF)
-        masterGraph.patch(Patch(addGraph=self.graph))
+        # config graph is too noisy; maybe make it a separate resource
+        #masterGraph.patch(Patch(addGraph=self.graph))
 
 class Board(object):
     """similar to arduinoNode.Board but without the communications stuff"""
     def __init__(self, graph, masterGraph, uri):
         self.graph, self.uri = graph, uri
-        self.ctx = ROOM['pi/%s' % hostname]
         self.masterGraph = masterGraph
         self.masterGraph.patch(Patch(addQuads=self.staticStmts()))
         self.pi = pigpio.pi()
@@ -52,39 +58,19 @@
         log.debug('found %s devices', len(self._devs))
         self._statementsFromInputs = {} # input device uri: latest statements
         self._carbon = CarbonClient(serverHost='bang')
-
+        for d in self._devs:
+            self.syncMasterGraphToHostStatements(d)
     def startPolling(self):
         task.LoopingCall(self._poll).start(.5)
 
     def _poll(self):
         for i in self._devs:
-            prev = inContext(self._statementsFromInputs.get(i.uri, []), self.ctx)
+            prev = inContext(self._statementsFromInputs.get(i.uri, []), CTX)
             new = self._statementsFromInputs[i.uri] = i.poll()
-            new = inContext(new, self.ctx)
+            new = inContext(new, CTX)
             self.masterGraph.patch(Patch.fromDiff(prev, new))
         self._exportToGraphite()
 
-    def outputStatements(self, stmts):
-        unused = set(stmts)
-        for dev in self._devs:
-            stmtsForDev = []
-            for pat in dev.outputPatterns():
-                if [term is None for term in pat] != [False, False, True]:
-                    raise NotImplementedError
-                for stmt in stmts:
-                    if stmt[:2] == pat[:2]:
-                        stmtsForDev.append(stmt)
-                        unused.discard(stmt)
-            if stmtsForDev:
-                log.info("output goes to action handler for %s" % dev.uri)
-                dev.sendOutput(stmtsForDev)
-                log.info("success")
-        if unused:
-            log.warn("No devices cared about these statements:")
-            for s in unused:
-                log.warn(repr(s))
-                
-    # needs merge with arduinoNode.py
     def _exportToGraphite(self):
         # note this is writing way too often- graphite is storing at a lower res
         now = time.time()
@@ -103,9 +89,42 @@
                         log.debug('  sending %s -> %s', stmt[0], graphiteName)
                         self._carbon.send(graphiteName, stmt[2].toPython(), now)
 
+    def outputStatements(self, stmts):
+        unused = set(stmts)
+        for dev in self._devs:
+            stmtsForDev = []
+            for pat in dev.outputPatterns():
+                if [term is None for term in pat] != [False, False, True]:
+                    raise NotImplementedError
+                for stmt in stmts:
+                    if stmt[:2] == pat[:2]:
+                        stmtsForDev.append(stmt)
+                        unused.discard(stmt)
+            if stmtsForDev:
+                log.info("output goes to action handler for %s" % dev.uri)
+                dev.sendOutput(stmtsForDev)
+
+                # Dev *could* change hostStatements at any time, and
+                # we're not currently tracking that, but the usual is
+                # to change them in response to sendOutput so this
+                # should be good enough. The right answer is to give
+                # each dev the masterGraph for it to write to.
+                self.syncMasterGraphToHostStatements(dev)
+                log.info("output and masterGraph sync complete")
+        if unused:
+            log.info("Board %s doesn't care about these statements:", self.uri)
+            for s in unused:
+                log.warn("%r", s)
+
+    def syncMasterGraphToHostStatements(self, dev):
+        hostStmtCtx = URIRef(dev.uri + '/host')
+        newQuads = inContext(dev.hostStatements(), hostStmtCtx)
+        p = self.masterGraph.patchSubgraph(hostStmtCtx, newQuads)
+        log.debug("patch master with these host stmts %s", p)
+
     def staticStmts(self):
-        return [(HOST[socket.gethostname()], ROOM['connectedTo'], self.uri, self.ctx)]
-                        
+        return [(HOST[hostname], ROOM['connectedTo'], self.uri, CTX)]
+
     def description(self):
         """for web page"""
         return {
@@ -114,6 +133,12 @@
             'graph': 'http://sticker:9059/graph', #todo
             }
         
+class Dot(cyclone.web.RequestHandler):
+    def get(self):
+        configGraph = self.settings.config.graph
+        dot = dotrender.render(configGraph, self.settings.boards)
+        self.write(dot)
+        
 def rdfGraphBody(body, headers):
     g = Graph()
     g.parse(StringInputSource(body), format='nt')
@@ -142,7 +167,7 @@
     def get(self):
         self.set_header('Content-type', 'application/json')
         self.write(json.dumps({
-            'host': socket.gethostname(),
+            'host': hostname,
             'boards': [self.settings.board.description()]
         }, indent=2))
         
@@ -158,11 +183,11 @@
         twlog.startLogging(sys.stdout)
 
         log.setLevel(logging.DEBUG)
-    
+
     masterGraph = PatchableGraph()
     config = Config(masterGraph)
 
-    thisHost = Literal(socket.gethostname())
+    thisHost = Literal(hostname)
     for row in config.graph.query(
             'SELECT ?board WHERE { ?board a :PiBoard; :hostname ?h }',
             initBindings=dict(h=thisHost)):
@@ -179,11 +204,11 @@
         (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),
-        (r'/boards', Boards),
-        #(r'/dot', Dot),
+        (r'/dot', Dot),
         ], config=config, board=board, debug=arg['-v']), interface='::')
     reactor.run()