--- /dev/null
+#!/bin/zsh
+
+DMX_SCALE=5
+DMX_SHIFT=20
+
+COMMON_ARGS=(
+ --end now
+ --width=960 --height=480
+ --lazy
+ --step=60 -v "degrees C" --upper-limit 32 --lower-limit 19 --rigid
+ --right-axis-label "DMX"
+ --right-axis ${DMX_SCALE}:-${DMX_SHIFT}
+ DEF:tempH=/home/pi/sc/data/heater-temp.rrd:temp:AVERAGE
+ DEF:tempHN=/home/pi/sc/data/hide-near-temp.rrd:temp:AVERAGE
+ DEF:dmxHN=/home/pi/sc/data/hide-near-dmx.rrd:dmx:AVERAGE
+ DEF:tempHF=/home/pi/sc/data/hide-far-temp.rrd:temp:AVERAGE
+ DEF:tempTN=/home/pi/sc/data/tank-near-temp.rrd:temp:AVERAGE
+ DEF:tempTF=/home/pi/sc/data/tank-far-temp.rrd:temp:AVERAGE
+ CDEF:scaled_dmxHN=dmxHN,${DMX_SHIFT},+,${DMX_SCALE},/
+ LINE:scaled_dmxHN\#000000:"dmx near"
+ LINE2:tempH\#FF0000:"heater"
+ LINE2:tempHN\#808000:"hide near"
+ LINE2:tempHF\#FF8000:"hide far"
+ LINE2:tempTN\#00FF00:"tank near"
+ LINE2:tempTF\#00FFFF:"tank far"
+ HRULE:30#800000
+ HRULE:25#8000FF
+ HRULE:21#0000FF
+)
+
+case "$QUERY_STRING" in
+ 4h) OUTFILE="4h.png"; PARAMS=(--start now-4h --title "Melman Vivarium Sensor Data 4h" ) ;;
+ 24h) OUTFILE="24h.png"; PARAMS=(--start now-24h --title "Melman Vivarium Sensor Data 24h") ;;
+ 7d) OUTFILE="7d.png"; PARAMS=(--start now-7d --title "Melman Vivarium Sensor Data 7d" ) ;;
+ 30d) OUTFILE="30d.png"; PARAMS=(--start now-30d --title "Melman Vivarium Sensor Data 30d") ;;
+ *)
+ cat <<HERE
+Content-Type: text/html
+Status: 404 Not Found
+
+<html><body>Invalid query parameter '${QUERY_STRING}'</body></html>
+HERE
+ exit 1
+esac
+
+rrdtool graph /home/pi/public_html/${OUTFILE} ${PARAMS[@]} ${COMMON_ARGS[@]} >&2
+
+cat <<HERE
+Location: /~pi/${OUTFILE}
+
+HERE
+
--- /dev/null
+#!/bin/zsh
+
+echo "Content-Type: text/plain"
+echo
+exec tail -n 40 /home/pi/sc/data/monitor.log/current
--- /dev/null
+#!/usr/bin/python
+
+# Inspired by code found at http://wannabe.guru.org/scott/hobbies/temperature/
+
+import os
+import rrdtool
+import sched
+import time
+
+owdev_logname = {
+ "/sys/bus/w1/devices/28-011620c718ee/w1_slave" : "/home/pi/sc/data/hide-far-temp.rrd",
+ "/sys/bus/w1/devices/28-011620f10dee/w1_slave" : "/home/pi/sc/data/hide-near-temp.rrd",
+ "/sys/bus/w1/devices/28-02161e26acee/w1_slave" : "/home/pi/sc/data/tank-far-temp.rrd",
+ "/sys/bus/w1/devices/28-03164712aaff/w1_slave" : "/home/pi/sc/data/heater-temp.rrd",
+ "/sys/bus/w1/devices/28-0416526de6ff/w1_slave" : "/home/pi/sc/data/tank-near-temp.rrd",
+}
+
+owdev_thresholds_heater1 = {
+ "/sys/bus/w1/devices/28-011620c718ee/w1_slave" : (23.0, 25.0), # hide far
+ "/sys/bus/w1/devices/28-02161e26acee/w1_slave" : (21.0, 25.0), # tank air far
+ "/sys/bus/w1/devices/28-0416526de6ff/w1_slave" : (23.0, 27.0), # tank air near
+}
+
+# owdev_thresholds_heater2 = {
+# "/sys/bus/w1/devices/28-011620f10dee/w1_slave" : (29.0, 29.5), # hide near
+# }
+
+def nop (*arg, **kwarg):
+ pass
+
+def with_ow_temp_fk_id(devfn, x, *arg, **kwarg):
+ print("WARNING: failed to read %s" % devfn)
+ return x
+
+def with_ow_temp(devfn, sk, fk, *arg, **kwarg):
+ with open(devfn) as devf:
+ devstr = devf.read()
+ devlines = devstr.split("\n")
+ if devlines[0].find("YES") > 0:
+ return sk(devfn,float((devlines[1].split(" ")[9])[2:]) / 1000, *arg, **kwarg)
+ return fk(devfn, *arg, **kwarg)
+
+
+def check_temps(sc):
+ print ("check temps init")
+ def check(devfn, temp, threshs, desire):
+ # Anything too hot: turn off the heater
+ if (temp > threshs[devfn][1]): desire = "OFF"
+ # No stated preference and something too cold, turn on the heater
+ elif (temp <= threshs[devfn][0] and desire == None): desire = "ON"
+ return desire
+
+ did_interact = False
+ desire = None
+ for devfn in owdev_thresholds_heater1:
+ desire = with_ow_temp(devfn, check, with_ow_temp_fk_id, owdev_thresholds_heater1, desire)
+ if desire is not None:
+ did_interact = True
+ print("Set heater 1 %s" % desire)
+ os.system("/home/pi/sc/bin/rpb.expect 4 %s | grep -A 5 -e 'Plug ' | tr -d '\015'" % desire)
+
+ # desire = None
+ # for devfn in owdev_thresholds_heater2:
+ # desire = with_ow_temp(devfn, check, with_ow_temp_fk_id, owdev_thresholds_heater2, desire)
+ # if desire is not None:
+ # did_interact = True
+ # print("Set heater 2 %s" % desire)
+ # os.system("/home/pi/sc/bin/rpb.expect 5 %s | grep -A 5 -e 'Plug ' | tr -d '\015'" % desire)
+
+ if not did_interact:
+ os.system("/home/pi/sc/bin/rpb.expect | grep -A 5 -e 'Plug ' | tr -d '\015'")
+
+ sc.enter(300, 1, check_temps, (sc,))
+ print ("check temps fini")
+
+def do_log_temp(devfn, temp):
+ print("temp log: %s => %f" % (devfn, temp))
+ rrdtool.update(owdev_logname[devfn], "N:" + ("%f" % temp))
+
+def read_temps(sc,itime):
+ for devfn in owdev_logname: with_ow_temp(devfn, do_log_temp, nop)
+ itime = itime + 60
+ sc.enterabs(itime, 2, read_temps, (sc,itime))
+
+itime = time.time()
+s = sched.scheduler(time.time, time.sleep)
+s.enterabs(itime, 1, check_temps, (s,))
+s.enterabs(itime, 2, read_temps, (s,itime))
+
+print("Monitor starting...")
+s.run()
--- /dev/null
+# Based on https://github.com/ivmech/ivPID/blob/master/PID.py
+# used under GPLv3 (or later)
+
+class PIDLoop(object):
+
+ def __init__ (self) :
+ self.kP = 0.0
+ self.kI = 0.0
+ self.kD = 0.0
+ self.kDDecay = 0.0
+ self.smooth_error_denom = 1.0
+
+ self.hard_max = None
+ self.hard_min = None
+
+ self.clear()
+
+ pass
+
+ def clear (self) :
+ self.last_upd_time = None
+ self.last_error = 0.0
+ self.sum_error = 0.0
+ self.smooth_error = 0.0
+
+ def setKP (self, kP) :
+ self.kP = kP
+
+ def setKD (self, kD, kDDecay) :
+ self.kD = kD
+ self.kDDecay = kDDecay
+ self.smooth_error_denom = 1 / (1 - kDDecay)
+
+ def setKI (self, kI) :
+ self.kI = kI
+
+ def setPoint (self, v) :
+ self.setpoint = v
+
+ def setHardMax (self, v) :
+ self.hard_max = v
+
+ def setHardMin (self, v) :
+ self.hard_min = v
+
+ def _value (self, error, edeltasmooth) :
+
+ print ("PID LOOP CONTRIBUTIONS: p=%r d=%r i=%r" % (self.kP * error, self.kD * edeltasmooth, self.kI * self.sum_error))
+
+ return (self.kP * error) + (self.kD * edeltasmooth) + (self.kI * self.sum_error)
+
+ def update (self, value, when) :
+ if self.setpoint is None :
+ raise ValueError("Setpoint not set")
+
+ # P
+ error = self.setpoint - value
+
+ # Time
+ tdelta = 0.0
+ if self.last_upd_time is not None :
+ tdelta = when - self.last_upd_time
+ if tdelta < 0 :
+ tdelta = 0.0
+
+ # D
+ edeltasmooth = 0.0
+ if tdelta > 0:
+ edeltasmooth = (error - (self.smooth_error/self.smooth_error_denom))/tdelta
+
+ # I (trapezoidal integration) with optional tests against a hard stop
+ ov = self._value(error, edeltasmooth)
+ sum_error_delta = tdelta*(self.last_error + error)/2.0
+
+ if self.hard_max is not None and ov >= self.hard_max and sum_error_delta > 0:
+ # Do not increment error; we're already slammed up against the hard limit
+ pass
+ elif self.hard_min is not None and ov <= self.hard_min and sum_error_delta < 0:
+ # Do not decrement error; we're already slammed up against the hard limit
+ pass
+ else if sum_error_delta != 0:
+ # Update sum_error and recompute the output value
+ self.sum_error += sum_error_delta
+ ov = self._value(error, edeltasmooth)
+
+ # Advance time
+ self.last_error = error
+ self.smooth_error *= self.kDDecay
+ self.smooth_error += error
+ self.last_upd_time = when
+
+ return ov
+
+ def __str__ (self) :
+ return "laste=%r smoothe=%r sume=%r" % (self.last_error, self.smooth_error/self.smooth_error_denom, self.sum_error)
+
+
+# A PID loop with hard limits on its behavior. Designed as a kind of fail-safe should
+# oscillations get out of hand or such.
+class PIDThresh(PIDLoop):
+
+ def __init__ (self, lowO, lowH, lowS, midO, highS, highH, highO) :
+ self.low_out = lowO
+ self.low_hard = lowH
+ self.low_soft = lowS
+ self.mid_out = midO
+ self.high_soft = highS
+ self.high_hard = highH
+ self.high_out = highO
+ self.override = None
+ super(PIDThresh,self).__init__()
+
+ def clear (self) :
+ super(PIDThresh,self).clear()
+
+ def update (self, value, when) :
+ if value > self.high_hard :
+ self.override = self.high_out
+ elif value < self.low_hard :
+ self.override = self.low_out
+ elif self.override is not None and self.low_soft <= value <= self.high_soft :
+ self.override = None
+ super(PIDThresh,self).clear()
+
+ if self.override : return self.override
+ else : return super(PIDThresh, self).update(value,when)
+
+ def __str__ (self) :
+ return super(PIDThresh,self).__str__()
--- /dev/null
+#!/usr/bin/python
+
+# Inspired by code found at http://wannabe.guru.org/scott/hobbies/temperature/
+
+import os
+import sched
+import time
+import serial
+import pidloop
+import rrdtool
+
+dmxdev = serial.Serial("/dev/serial/by-id/usb-DMXking.com_DMX_USB_PRO_6A0SVM7J-if00-port0", 57600);
+
+loop_hidenear = pidloop.PIDThresh(128,26,28,0,31,33,-128)
+loop_hidenear.setPoint(30)
+loop_hidenear.setHardMax(128)
+loop_hidenear.setHardMin(-128)
+loop_hidenear.setKP(60.0)
+loop_hidenear.setKI(0.004)
+loop_hidenear.setKD(1000.0,0.95)
+loop_hidenear.sum_error = -3000.0 # XXX Initialize offset point a bit
+
+def with_ow_temp_fk_id(devfn, loop, s, *arg, **kwarg):
+ print("WARNING: failed to read %s" % devfn)
+ return s # an ugly default
+
+def with_ow_temp(devfn, sk, fk, *arg, **kwarg):
+ with open(devfn) as devf:
+ devstr = devf.read()
+ devlines = devstr.split("\n")
+ if devlines[0].find("YES") > 0:
+ return sk(devfn,float((devlines[1].split(" ")[9])[2:]) / 1000, *arg, **kwarg)
+ return fk(devfn, *arg, **kwarg)
+
+def check_temps(sc):
+ sc.enter(10, 1, check_temps, (sc,))
+
+ def check(devfn, temp, loop, s, offset, rrd):
+ desire = 128.0 + loop.update(temp, time.time())
+
+ if desire < 0 :
+ desire = 0
+ loop.output = 0
+ elif desire > 255 :
+ desire = 255
+ loop.output = 255
+
+ rrdtool.update(rrd, "N:" + ("%f" % desire))
+
+ return s[:offset] + chr(int(desire+0.5)) + s[offset+1:]
+
+ # DMX conttrol string; initialize to all channels full off
+ # 7E -- header
+ # 06 -- type
+ # 03 00 -- payload length
+ # 00 -- channel 0 value (ignored by hardware)
+ # 00 -- channel 1 value
+ # 00 -- channel 2 value
+ # E7 -- footer
+ s = "\x7E\x06\x03\x00\x00\x00\x00\xE7"
+
+ # Drive loop
+ s = with_ow_temp("/sys/bus/w1/devices/28-011620f10dee/w1_slave",
+ check, with_ow_temp_fk_id, loop_hidenear, s, 5, "/home/pi/sc/data/hide-near-dmx.rrd")
+
+ print ("check temps fini: out=%r lhn=(%s)" % (s, loop_hidenear))
+ assert(dmxdev.write(s) == 8)
+
+itime = time.time()
+s = sched.scheduler(time.time, time.sleep)
+s.enterabs(itime, 1, check_temps, (s,))
+
+print("Monitor starting...")
+s.run()
--- /dev/null
+#!/usr/bin/expect
+
+proc help {} {
+ global argv0
+ global actionnames
+ send_user "usage: $argv0 outlet {on|off|boot}\n"
+ exit 1
+}
+
+proc waitprompt {} {
+ expect {
+ "RPB+> " { }
+ timeout {
+ send_user " ===> TIMEOUT <===\n"
+ exit 1
+ }
+ }
+}
+
+spawn /home/pi/sc/bin/serial.wrap [ exec readlink -f /dev/serial/by-path/platform-20980000.usb-usb-0:1.2:1.0-port0 ]
+set timeout 70
+send "\n"
+waitprompt
+
+if {$argc == 2} {
+ set outlet [lindex $argv 0]
+ set cmd [string toupper [lindex $argv 1]]
+
+ send "/$outlet $cmd\n"
+ set timeout 30
+ waitprompt
+}
--- /dev/null
+#!/bin/zsh
+
+RRDS=(
+ heater-temp
+ hide-near-temp
+ hide-far-temp
+ tank-near-temp
+ tank-far-temp
+)
+
+ARGS=(
+ --no-overwrite
+ --step 60
+ DS:temp:GAUGE:900:-10:50
+ RRA:AVERAGE:0.5:1:525600
+ RRA:AVERAGE:0.25:60:87600
+ RRA:MIN:0.025:60:87600
+ RRA:MAX:0.025:60:87600
+)
+
+for rrd in ${=RRDS[@]}; do
+ rrdtool create /home/pi/sc/data/${rrd}.rrd ${=ARGS[@]}
+done
+
+RRDS=(
+ hide-near-dmx
+)
+
+ARGS=(
+ --no-overwrite
+ --step 10
+ DS:dmx:GAUGE:900:0:255
+ RRA:AVERAGE:0.5:6:525600
+ RRA:AVERAGE:0.25:360:87600
+ RRA:MIN:0.025:360:87600
+ RRA:MAX:0.025:360:87600
+)
+
+for rrd in ${=RRDS[@]}; do
+ rrdtool create /home/pi/sc/data/${rrd}.rrd ${=ARGS[@]}
+done
--- /dev/null
+#!/bin/sh
+
+set -e -u
+
+stty -F $1 sane -echo clocal crtscts hupcl -icanon icrnl inlcr 9600
+stty -F $1 0 2>/dev/null || true
+stty -F $1 sane -echo clocal crtscts hupcl -icanon icrnl inlcr 9600
+exec flock -e -w 60 /var/lock/LCK..`basename $1` socat $1 STDIO
--- /dev/null
+*.rrd
+monitor.log
--- /dev/null
+SET $LongDeg 76
+SET $LongMin 36
+SET $LongSec 33
+SET $LatDeg 39
+SET $LatMin 17
+SET $LatSec 33
+
+REM AT [sunrise()] MSG Lights on...
+REM AT [sunrise()] RUN /home/pi/sc/bin/rpb.expect 1 on > /dev/null
+
+REM AT [sunset()] MSG Lights off...
+REM AT [sunset()] RUN /home/pi/sc/bin/rpb.expect 1 off > /dev/null
--- /dev/null
+#!/bin/sh
+exec chpst -u pi svlogd -tt /home/pi/sc/data/monitor.log
--- /dev/null
+#!/bin/sh
+exec chpst -u pi:pi:dialout python -u /home/pi/sc/bin/monitor.py
--- /dev/null
+#!/bin/sh
+
+exec chpst -u pi:pi:dialout remind -z /home/pi/sc/remind