changeset 1026:8e075449ba0a

piNode support for temp sensors. proper hostname lookup Ignore-this: a68c3319bffb16c55cbb5f329118f0a4 darcs-hash:3b4c8482b870731064ae5685d32a4206b7a2d7d6
author drewp <drewp@bigasterisk.com>
date Mon, 18 Jan 2016 22:43:22 -0800
parents f58b5536f683
children 208b960fde31
files service/piNode/config.n3 service/piNode/devices.py service/piNode/piNode.py
diffstat 3 files changed, 155 insertions(+), 12 deletions(-) [+]
line wrap: on
line diff
--- a/service/piNode/config.n3	Sun Jan 03 02:32:46 2016 -0800
+++ b/service/piNode/config.n3	Mon Jan 18 22:43:22 2016 -0800
@@ -43,4 +43,31 @@
 #  :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	Sun Jan 03 02:32:46 2016 -0800
+++ b/service/piNode/devices.py	Mon Jan 18 22:43:22 2016 -0800
@@ -1,20 +1,23 @@
 from __future__ import division
 
-import time, logging
+import time, logging, os
 from rdflib import Namespace, RDF, URIRef, Literal
 
 try:
     import pigpio
 except ImportError:
     pigpio = None
+import w1thermsensor
     
+import sys
 
 log = logging.getLogger()
 ROOM = Namespace('http://projects.bigasterisk.com/room/')
 XSD = Namespace('http://www.w3.org/2001/XMLSchema#')
+RDFS = Namespace('http://www.w3.org/2000/01/rdf-schema#')
 
 class DeviceType(object):
-    deviceType = None
+    deviceType = NotImplementedError
     @classmethod
     def findInstances(cls, graph, board, pi):
         """
@@ -23,6 +26,7 @@
         but two sensors on the same onewire bus makes only one device
         (which yields more statements).
         """
+        log.debug("graph has any connected devices of type %s?", cls.deviceType)
         for row in graph.query("""SELECT ?dev ?gpioNumber WHERE {
                                     ?board :hasPin ?pin .
                                     ?pin :gpioNumber ?gpioNumber;
@@ -30,8 +34,7 @@
                                     ?dev a ?thisType .
                                   } ORDER BY ?dev""",
                                initBindings=dict(board=board,
-                                                 thisType=cls.deviceType),
-                               initNs={'': ROOM}):
+                                                 thisType=cls.deviceType)):
             yield cls(graph, row.dev, pi, int(row.gpioNumber))
 
     def __init__(self, graph, uri, pi, pinNumber):
@@ -91,6 +94,8 @@
 
 @register
 class MotionSensorInput(DeviceType):
+    # compare motion sensor lib at http://pythonhosted.org/gpiozero/inputs/
+    # which is a bit fancier
     deviceType = ROOM['MotionSensor']
 
     def setup(self):
@@ -126,6 +131,9 @@
 @register
 class RgbStrip(DeviceType):
     """3 PWMs for r/g/b on a strip"""
+    # pigpio daemon is working fine, but
+    # https://github.com/RPi-Distro/python-gpiozero/blob/59ba7154c5918745ac894ea03503667d6473c760/gpiozero/output_devices.py#L213
+    # can also apparently do PWM
     deviceType = ROOM['RgbStrip']
     
     @classmethod
@@ -183,6 +191,82 @@
 
 
 @register
+class TempHumidSensor(DeviceType):
+    deviceType = ROOM['TempHumidSensor']
+
+    def __init__(self, *a, **kw):
+        DeviceType.__init__(self, *a, **kw)
+        sys.path.append('/opt/pigpio/EXAMPLES/Python/DHT22_AM2302_SENSOR')
+        import DHT22
+        self.sensor = DHT22.sensor(self.pi, self.pinNumber)
+    
+    def poll(self):
+        self.sensor.trigger()
+        humid, tempC = self.sensor.humidity(), self.sensor.temperature()
+
+        stmts = set()
+        if humid is not None:
+            stmts.add((self.uri, ROOM['humidity'], Literal(humid)))
+        else:
+            stmts.add((self.uri, RDFS['comment'],
+                       Literal('DHT read returned None')))
+        if tempC is not None:
+            stmts.add((self.uri, ROOM['temperatureF'],
+                       Literal(tempC * 9 / 5 + 32)))
+        else:
+            stmts.add((self.uri, RDFS['comment'],
+                       Literal('DHT read returned None')))
+        return stmts
+        
+    def watchPrefixes(self):
+        return [
+            (self.uri, ROOM['temperatureF']),
+            (self.uri, ROOM['humidity']),
+        ]
+
+@register
+class OneWire(DeviceType):
+    """
+    Also see /my/proj/ansible/roles/raspi_io_node/tasks/main.yml for
+    some system config that contains the pin number that you want to
+    use for onewire. The pin number in this config is currently ignored.
+    """
+    deviceType = ROOM['OneWire']
+    # deliberately written like arduinoNode's one for an easier merge.
+    def __init__(self,  *a, **kw):
+        DeviceType.__init__(self, *a, **kw)
+        log.info("scan for w1 devices")
+        self._sensors = w1thermsensor.W1ThermSensor.get_available_sensors()
+        for s in self._sensors:
+            # Something looks different about these ids
+            # ('000003a5a94c') vs the ones I get from arduino
+            # ('2813bea50300003d'). Not sure if I'm parsing them
+            # differently or what.
+            s.uri = URIRef(os.path.join(self.uri, 'dev-%s' % s.id))
+            log.info('  found temperature sensor %s' % s.uri)
+        
+    def poll(self):
+        try:
+            stmts = []
+            for sensor in self._sensors:
+                stmts.append((self.uri, ROOM['connectedTo'], sensor.uri))
+                try:
+                    tempF = sensor.get_temperature(sensor.DEGREES_F)
+                    stmts.append((sensor.uri, ROOM['temperatureF'],
+                                  Literal(tempF)))
+                except w1thermsensor.core.SensorNotReadyError as e:
+                    log.warning(e)
+
+            return stmts
+        except Exception as e:
+            log.error(e)
+            os.abort()
+            
+    def watchPrefixes(self):
+        return [(s.uri, ROOM['temperatureF']) for s in self._sensors]
+
+        
+@register
 class LedOutput(DeviceType):
     deviceType = ROOM['LedOutput']
                 
@@ -219,7 +303,7 @@
         for row in graph.query('''SELECT DISTINCT ?uri WHERE {
           ?board :onboardDevice ?uri . 
           ?uri a :OnboardTemperature .
-        }'''):
+        }''', initBindings=dict(board=board)):
             yield cls(graph, row.uri, pi, pinNumber=None)
     
     def poll(self):
--- a/service/piNode/piNode.py	Sun Jan 03 02:32:46 2016 -0800
+++ b/service/piNode/piNode.py	Mon Jan 18 22:43:22 2016 -0800
@@ -1,5 +1,5 @@
 from __future__ import division
-import sys, logging, socket, json
+import sys, logging, socket, json, time
 import cyclone.web
 from rdflib import Namespace, URIRef, Literal, Graph, RDF
 from rdflib.parser import StringInputSource
@@ -10,7 +10,7 @@
 sys.path.append("../../../../site/magma")
 
 from stategraph import StateGraph
-sys.path.append('/home/pi/dim/PIGPIO')
+sys.path.append('/opt/pigpio')
 try:
     import pigpio
 except ImportError:
@@ -21,6 +21,9 @@
 
 import devices
 
+# from /my/proj/room
+from carbondata import CarbonClient
+
 log = logging.getLogger()
 logging.getLogger('serial').setLevel(logging.WARN)
 ROOM = Namespace('http://projects.bigasterisk.com/room/')
@@ -33,7 +36,7 @@
         self.graph = Graph()
         log.info('read config')
         self.graph.parse('config.n3', format='n3')
-        self.graph.bind('', ROOM) # not working
+        self.graph.bind('', ROOM) # maybe working
         self.graph.bind('rdf', RDF)
 
 class GraphPage(cyclone.web.RequestHandler):
@@ -58,6 +61,7 @@
         self._devs = devices.makeDevices(graph, self.uri, self.pi)
         log.debug('found %s devices', len(self._devs))
         self._statementsFromInputs = {} # input device uri: latest statements
+        self._carbon = CarbonClient(serverHost='bang')
 
     def startPolling(self):
         task.LoopingCall(self._poll).start(.5)
@@ -65,7 +69,8 @@
     def _poll(self):
         for i in self._devs:
             self._statementsFromInputs[i.uri] = i.poll()
-        
+        self._exportToGraphite()
+
     def outputStatements(self, stmts):
         unused = set(stmts)
         for dev in self._devs:
@@ -85,6 +90,25 @@
             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()
+        # 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']}
+        # bug: one sensor can have temp and humid- this will be ambiguous
+        for s, graphiteName in self.graph.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 currentGraph(self):
         g = Graph()
@@ -154,8 +178,16 @@
         # notify reasoning
         pass
 
-    thisBoard = URIRef('http://bigasterisk.com/homeauto/node2')
-    
+    thisHost = Literal(socket.gethostname())
+    for row in config.graph.query(
+            'SELECT ?board WHERE { ?board a :PiBoard; :hostname ?h }',
+            initBindings=dict(h=thisHost)):
+        thisBoard = row.board
+        break
+    else:
+        raise ValueError("config had no board for :hostname %r" % thisHost)
+
+    log.info("found config for board %r" % thisBoard)
     board = Board(config.graph, thisBoard, onChange)
     board.startPolling()