HouseMade - The Hurst HouseHold Heater Helpmate
A.J.Hurst
Version 1.3.6
20160204:171508
Table of Contents
1. Introduction
These programs have been distilled from Nathan Hurst's original
Nautilus Shell suite of programs. The main differences are a)
the change of name, b) the documentation, and c) consistency in
logging structures (where possible).
1.1 Overview
There are a number of programs in this suite, and they are
grouped into the following categories:
- The Web Interface
(operational)
- Provides an easy to use interface to the program
suite, using two key programs: house and
timer. These both invoke the house computer using
the Flask protocol.
- The Heating System
(operational)
- This subsystem controls the house heating system.
Both the temperature and timing may be controlled: the
temperature in steps of 1 degree Celsius, form 10 to 26
degrees, and the time in steps of 5 minutes from midnight
to midnight, 7 (distinct) days of the week. Up to 7 time
blocks per day are permitted.
- The Tank System
(operational)
- Provides monitoring of the water storage facilities.
- The Solar System
(operational)
- Provides monitoring of the solar photovoltaic systems,
together with the inverter and UPS sub-systems.
- The Chook Door System
(operational)
- Controls the opening and shutting of the Chook House
Door.
- The House Computer
(obsolete)
- Provides logging of house data, as well as an RPC
interface to access the logged data. This functionality
has been subsumed by being incorporated into each
subsystem, and a separate logging system is no longer
used.
- The
House Data Server and Relay Control System
(operational)
- Uses an Arduino driving 8 relays to switch
the various circuits.
1.2 TODOs
-
Write a "startEverything" script
-
Fix pl60.c code to use getDevice to find the solar
USB port (or rewrite it in Python).
-
add simple 'flash' mode watering
-
design and implement web interface to watering cron activity
1.3 History
The original House Computer was set up on an Intel 386 box,
named redfern, in keeping with my philosophy of naming
all my computers after railway junctions. But redfern did not
have enough expansion capability, and used the rapidly dating
EISA bus architecture, so it was not long before it was
replaced with a larger chassis using a 486 and PCI bus. This
machine was named central, and was the mainstay of the
house computer system for many years.
The earliest evidence for the operation of these two systems
is a file dated 23 May 1999, which I believe was written for
the central system. Whatever, by mid 2001 the central system
was certainly running, which makes me think that redfern dates
are from early 1999, while central was probably commissioned
in late 1999 to early 2000. Central was a single system, and
ran a suite of programs known as Nautilus (aka "Water Shell",
originally because it was a "shell" controlling just the
garden watering operations), serving both the web page control
systems (through Apache and locally written cgi scripts), as
well as the various logging subsystems (temperature, humidity,
solar panel insolation, and rainwater tank levels.
Central's disk system was the weakest link in the system,
since it died in June 2012, some 12 years after commissioning.
This is regarded as a fairly robust operation, and set the
benchmark for future systems.
Before it died, however, work had been underway to replace
the logging operations through a low-power mini-controller 486
based system, known as garedelyon, with the original
intention to move all functionality to garedelyon. However,
garedelyon's operating system was not up to running a full web
server, and so it became just the data logging component of
the system. Garedelyon had several RS232 and USB ports, and
took over the responsibility for performing all the logging
operations. A remote RPC mechanism allowed the web server
system to communicate data between the two systems. Once
central had died, the web serving functions were moved to
various other machines, ending up on an old PowerBook Apple
laptop, known as ringwood.
In January 2015, while we we away overseas, ringwood's
battery died, taking the system, including the mini-controller
garedelyon and weather station, with it. While I had a spare
mini-controller, it was not worth replacing the laptop, and it
was decided to move the entire system back to a single
controller, based upon the new Beaglebone Black that I had
acquired while overseas. This new machine was known as
orsay.
In the meantime, while the hardware for orsay was being
developed, my Acer laptop known as lilydale was pressed
into service, this time running a limited number of functions.
Part of this limitation was due to there no longer being any
way to communicate with RS232 ports, as used by the
weather/tank/solar logging systems on garedelyon. A major
part of the hardware redesign occasioned by the switch to
using a Beaglebone was the need to run a USB Hub, and to
connect all the RS232 ports via RS232/USB dongles.
The re-design of the system was sufficiently complete by mid
April 2015 to bring it on-line. Major changes include moving
all functionality to the one system (thus reverting to a
similar framework to the original Central/Nautilus system),
and a shift to using Flask as the main web interface. This
major rewrite was renamed HouseMade because of its much
wider functionality, and now controls all of the house
heating, the watering system, logging and display of house
data (solar, water tank), the web interfaces, and most
recently, the chook house door. Documentation for this system
is this current document.
Unfortunately orsay was a little unreliable, crashing
more frequently than it should, and eventually dying
altogether in mid Jun 2015 (exact date not recorded). The
replacement machine (known as bastille) suffered
similar problems, and failed on 10 Jul 2015. The reasons for
these failures are not clear, but are thought to be related to
power supply instabilities.
Rather than risk another $100 piece of hardware, I have
reverted (ostensibly temporarily) to using my Acer laptop,
running Ubuntu 14.04, "Trusty Tahr". This did require a few
days work to get it going properly, but there have been a
number of significant improvements as a consequence:
- The tank logging has been migrated to a Python program,
thus creating the potential for some more smarts in that
subsection.
- The USB issues previously identified have been resolved.
See section USB Resolution.
These issues were all a consequence of the need to eliminate
all RS232 code, and switch to a USB only system. The
Beaglebone Black is known to have a significant bug in its
USB subsystem, and this may have contributed to the
unreliable behaviour reported above.
- The chook door mechanism has been implemented, and is
operational.
- The hostname had been hardcoded into the code, since I
did not expect that it would be changing so rapidly. It has
now been factored out, and replaced by the abstract title of
central, a tribute to the original system which
lasted for some 12 years.
1.4 Philosophies
The house computer complex is just that - complex. Some
design principles are in order. One of the difficulties of
design has been the need to maintain several different
systems, for different reasons. The main systems are (in
abstract terms): the data logger, the house
controller, and the house web server. Currently the system is
structured so that all of these functions are undertaken by
the machine lilydale (see previous section History).
The first principle is then all data logging to be handled
by the data logging system (as far as possible). Where
this is not possible, the system responsible for collecting
the data should transfer it to the logging system as soon as
possible, and the logging system should be regarded as the
definitive source for any data requests. While this principle
was originally framed to permit it to be handled by a separate
machine, it can reside anywhere on the house network.
The second principle is that any request to change the
state of the house must be passed through the house
controller, even if it is not the final direct control
mechanism. The state of the house is defined by the state of
the (currently) 12 relays controlled by the controller
system. Again, there is no requirement that this
functionality be co-located with the others.
The third principle is that all web traffic is handled by
the web system. A key factor in deciding how this
functionality is handled is whether the web server is secure
enough (can external users change the heating or open the
chook door, for example), and secondly whether it could handle
the additional traffic imposed by an externally available web
server.
2. Key Data Structures
2.1 Edit Warning
Provide a simple inclusion to flag that all derived files are
extracted from this file, rather than stand-alone.
<edit warning 2.1> =## **********************************************************
## * do NOT EDIT THIS FILE! *
## * Use $HOME/Computers/House/HouseMade.xlp instead *
## **********************************************************
2.2 House Definitions Module
The house definitions module, HouseDefinitions.py,
gathers together in one place those constants that are common to
all systems. Note that no shared variables may be handled by
this module, since it is not shared across the various systems
as a single instance. (All declared "variables" are actually
constants, but python does not allow explicit constant
declarations.)
Following a second failure of a Beaglebone, the house
computer has reverted to the Acer laptop. This is now possible
because of all the work done in getting the USB data inputs
working. But it has also forced my to rethink the heavy use of
hardcoding the machine name into all these script, hence the new
definition of CENTRAL (the name is taken from the
original house server).
"HouseDefinitions.py" 2.2 =import xmlrpclib
CENTRAL="lilydale"
# system-wide definition of the house-controlling relay complement
RelayNames=[
'ChookUp', # 0 - the order of these is important
'ChookDown', # 1
'Unused2', # 2
'TopUp', # 3
'BottomVegBed', # 4
'MiddleVegBed', # 5
'TopVegBed', # 6
'CarportVegBed', # 7
'RainForest', # 8
'Woo2Plas', # 9
'FloodNDrain', # 10
'Heating' # 11
]
NumberOfRelays = len(RelayNames)
RelayTable={}
for i in range(NumberOfRelays):
RelayTable[RelayNames[i]]=i
ThermostatSetting=19
NTempBlocks=7 # max number of distinct temperature blocks allowed
HServer='http://%s:5000/heating' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
colours=['#00f', # 10
'#04f','#08f','#0cf','#0ff', # 11-14
'#0fc','#0f8','#0f4','#0f0', # 15-18
'#4f0','#8f0','#cf0','#ff0', # 19-22
'#fc0','#f80','#f40','#f00'] # 23-26
CentralGood=True
Central=xmlrpclib.ServerProxy('http://%s:8001' % CENTRAL)
# check that the server is running by testing one of its interfaces
try:
Central.getState()
except:
# bad response, let users know
CentralGood=False
def setColourOld(temp):
# return colours[temp-10]
if temp>=ThermostatSetting:
return 'red'
else:
return 'blue'
def setTemperatureOld(arg):
t=int(arg)
if t>ThermostatSetting: t=ThermostatSetting
if t<ThermostatSetting: t=10
return t
def setColour(temp):
return colours[temp-10]
def setTemperature(arg):
t=int(arg)
return t
The routines setColour and setTemperature are
defined to localize these two calculations for the
house and timer modules. They will be revised
once the temperature adjustment system is rebuilt to its full
potential.
3. The Web Interface
The web interface is Flask application running on the house
computer lilydale, and providing a convential web page
via a port 5000 call (the
Flask port number), and interfaces to the house and timer
modules through house and heating respectively. The
figure '10' in setTemperature is just the lowest
temperature that is displayed by the timer web interface.
To preserve security of the system, and prevent unauthorized
access, this web server will only operate house functions if it
is invoked from a machine on the 10.0.0 network (the private
house network). In the longer term, username/password authority
may be added.
3.1 The Flask Application
"lilydaleFlask.py" 3.1 =#!/usr/bin/python
from flask import Flask,request
import cgi
import datetime
import os
import os.path
import re
from subprocess import PIPE,Popen
import xml.dom.minidom
import currentState
import HouseMade
import HouseDefinitions
#import TimerModule
import HeatingModule
XSLTPROC="/usr/bin/xsltproc"
COUNTERS="/home/ajh/local/central/counters"
debug=True
logging=True
def logMsg(msg,NewLine=True):
if NewLine: msg+='\n'
if logging:
logfile=open('/home/ajh/logs/central/flask.log','a')
logfile.write(msg)
logfile.close()
else:
print msg,
app=Flask(__name__)
<Flask: define the various flask URLs and calls 3.2>
<Flask: define the showpage routine 3.3>
if __name__=='__main__':
app.debug=debug
app.run(host='10.0.0.112')
logfile.close()
3.1.1 Define the Various Flask URLs and Calls
<Flask: define the various flask URLs and calls 3.2> =@app.route('/house')
def house():
argDict=request.args.to_dict(flat=False)
remadr=request.environ['REMOTE_ADDR']
server=request.environ['SERVER_NAME']
logMsg("%s@%s: house arguments=%s" % (server,remadr,argDict))
return HouseMade.
house(remadr,server,argDict)
@app.route('/weather')
def weather():
argDict=request.args.to_dict(flat=False)
remadr=request.environ['REMOTE_ADDR']
logMsg("%s: weather arguments=%s" % (remadr,argDict))
return HouseMade.
weather(argDict)
@app.route('/heating')
def heating():
argDict=request.args.to_dict(flat=False)
remadr=request.environ['REMOTE_ADDR']
logMsg("%s: heating arguments=%s" % (remadr,argDict))
return HeatingModule.
heating(logMsg,remadr,argDict)
@app.route('/solar')
def solar():
argDict=request.args.to_dict(flat=False)
remadr=request.environ['REMOTE_ADDR']
logMsg("%s: solar arguments=%s" % (remadr,argDict))
return HouseMade.
solar(argDict)
@app.route('/tank')
def tank():
argDict=request.args.to_dict(flat=False)
remadr=request.environ['REMOTE_ADDR']
logMsg("%s: tank arguments=%s" % (remadr,argDict))
return HouseMade.
tank(argDict)
@app.route('/chooks')
def chooks():
render=""
house=currentState.HouseState()
house.load()
state=house.get('chookDoorProve').upper()
if state=='OPEN': colour='red'
else: colour='green'
content='<meta http-equiv="Refresh" content="60">\n'
content+="<table border='1' width='100%%' height='100%%' bgcolor='%s'>\n" % (colour)
content+="<tr width='100%%'>\n"
content+="<td align='center' style='font-size:100px'>\n"
content+="%s\n</td>\n</tr>\n</table>" % (state)
render="<HTML>\n<BODY>\n%s\n</BODY>\n</HTML>\n" % (content)
return render
These two routines define the principal house web pages:
house is the general house page, and timer is
the heating timer control page. Note that timer has
now (v1.3.0) been replaced by heating.
3.1.2 Define the Showpage Routine
The following routine provides a general purpose webpage server
<Flask: define the showpage routine 3.3> =
The showpage routine does a similar task to that of
my index.py routine in the Apache web servers that I run.
It determines the type of file to be served, and in the case
of XML files, calls the xsltproc processor to convert
them to HTML.
<Flask: check what kind of file is requested 3.4> =# check what kind of file is requested
res=re.match('(.*)\.([^.]+)$',requestedFile)
if res:
reqtype=res.group(2)
if reqtype!='xml':
#logMsg("%s: Handling type %s" % (tsstring,reqtype))
reqf=open(requestedFile)
result=reqf.read()
return result
render=""
# define the parameters to the translation
filename=pagename
server='%s:5000' % HouseDefinitions.CENTRAL
host=HouseDefinitions.CENTRAL
BASE='(not used)'
filestat=os.stat(requestedFile)
filemod=filestat.st_mtime
dtfilemod=datetime.datetime.fromtimestamp(filemod)
dtstring=dtfilemod.strftime("%Y%m%d:%H%M")
URL="(dummy URL)"
<Flask: compute the current working directory 3.5> =# work out the relative current working directory
logMsg("requestedFile=%s" % (requestedFile))
res=filepat.match(requestedFile)
if res:
logMsg("matched")
dir=res.group(1)
relcwd=dir
# protocol for relcwd:
# no subdir => relcwd = '' (empty)
# exists subdir => relcwd = subdir (no leading or trailing slash)
#if dir!="":
# requestedFile=dir+'/'+res.group(2)
#else:
# requestedFile=res.group(2)
filename=res.group(2)
else:
logMsg("not matched")
# not ajh (sub)directory, extract full directory path
dir=os.path.dirname(requestedFile)
relcwd=dir
filename=os.path.basename(requestedFile)
#if relcwd[0:3]=='www/':
# relcwd=relcwd[4:]
logMsg("requestedFile=%s" % (requestedFile))
logMsg("relcwd=%s" % (relcwd))
<Flask: handle page counter information 3.6> =#logMsg("relfile = %s" % (relfile))
counterName=re.sub("/~ajh/",'',relfile)
counterName=re.sub("^/",'',counterName)
extnPattern=re.compile("(.xml)|(.html)")
counterName=re.sub(extnPattern,'',counterName)
counterName=COUNTERS+'/'+re.sub("/","-",counterName)
#logMsg("got counter name = %s" % (counterName))
newCounterStr='<?xml version="1.0"?>\n'
newCounterStr+='<counter><value>0</value><date>%s</date></counter>' % todayStr
try:
counterFile=open(counterName,'r')
dom=xml.dom.minidom.parse(counterFile)
counterFile.close()
except IOError:
dom=xml.dom.minidom.parseString(newCounterStr)
except xml.parsers.expat.ExpatError:
dom=xml.dom.minidom.parseString(newCounterStr)
except:
logMsg("Unexpected error:", sys.exc_info()[0])
raise
# now extract count field and update it
countNode=dom.getElementsByTagName('value')[0]
if countNode.nodeType == xml.dom.Node.ELEMENT_NODE:
textNode=countNode.firstChild
if textNode.nodeType == xml.dom.Node.TEXT_NODE:
text=textNode.nodeValue.strip()
countVal=int(text)
countVal=countVal+1
textNode.nodeValue="%d" % (countVal)
countDate='(unknown)'
countNode=dom.getElementsByTagName('date')[0]
if countNode.nodeType == xml.dom.Node.ELEMENT_NODE:
textNode=countNode.firstChild
if textNode.nodeType == xml.dom.Node.TEXT_NODE:
countDate=textNode.nodeValue.strip()
# write updated counter document
try:
counterFile=open(counterName,'w')
except IOError:
counterName='/home/ajh/local/localhost/counters/index'
counterFile=open(counterName,'w')
domString=dom.toxml()
counterFile.write(domString)
counterFile.close()
#logMsg("domString=%s" % (domString))
<Flask: collect parameters for XSLT translation 3.7> =parms=""
parms+="--param xmltime \"'%s'\" " % (dtstring)
parms+="--param htmltime \"'%s'\" " % (tsstring)
parms+="--param filename \"'%s'\" " % (filename)
parms+="--param relcwd \"'%s'\" " % (relcwd)
parms+="--param URL \"'%s'\" " % (URL)
parms+="--param today \"'%s'\" " % (todayStr)
parms+="--param host \"'%s'\" " % (host)
parms+="--param server \"'%s'\" " % (server)
parms+="--param base \"'%s'\" " % (BASE)
parms+="--param debug \"'%s'\" " % (debug)
logMsg(parms)
<Flask: process the XSLT translation 3.8> =# start a pipe to process the XSLT translation
# first see if there is a stylesheet
rfile=open(requestedFile,'r')
i=5
xsltrans="ajhwebpage.xsl"
for l in rfile.readlines():
#logMsg(l)
if i<=0: break
res=re.match('.*xml-stylesheet.*file://.*lib/xsl/(.*)"',l)
if res:
xsltrans=res.group(1)
break
i-=1
rfile.close()
xslfile="/home/ajh/www/lib/xsl/%s" % (xsltrans)
cmd=XSLTPROC+" --xinclude %s%s %s " % (parms,xslfile,requestedFile)
#render+="<p>%s</p>" % (cmd)
#(pipein,pipeout,pipeerr)=os.popen3(cmd)
#
# This is the crucial XML -> HTML conversion process!
#
pid=Popen(cmd,shell=True,stdout=PIPE,stderr=PIPE,close_fds=True)
#
# now handle what the conversion has done
#
(pipeout,pipeerr)=(pid.stdout,pid.stderr)
if debug:
cwd=os.getcwd()
logMsg("%s: (cwd:%s) %s" % (tsstring,cwd,cmd))
#sys.stderr.write("(cwd:%s) %s: %s\n" % (cwd,tsstring,cmd))
# report the fact, and the context (debugging purposes)
logMsg("%s: converting %s with %s\n" % (tsstring,requestedFile,xslfile))
<Flask: save html and process any errors 3.9> =# open a file to save the HTML for debug purposes
htmlfilename="/tmp/last.html"
htmlf=open(htmlfilename,'w')
for line in pid.stdout.readlines():
render+=line
htmlf.write(line)
htmlf.close()
errs=[]
for line in pipeerr.readlines():
errs.append(line)
#logMsg(line)
#logfile=PRIVATE+'/xmlerror.log'
#logfiled=open(logfile,'a')
if errs:
#logMsg("%s: ERROR IN REQUEST: %s" % (tsstring,requestedFile))
#logMsg("<HR/>\n")
logMsg("%s: MESSAGES GENERATED BY: %s" % (tsstring,requestedFile))
#logMsg("<PRE>")
for errline in errs:
#logfiled.write("%s: %s" % (tsstring,errline))
errline=cgi.escape(errline)
errline=errline.rstrip()
logMsg("%s: %s" % (tsstring,errline))
#logMsg("</PRE>")
#logMsg("<p>Please forward these details to ")
#logMsg("<a href='mailto:ajh@csse.monash.edu.au'>John Hurst</a>")
else:
#logfiled.write("%s: NO ERRORS IN %s\n" % (tsstring,requestedFile))
pass
#logfiled.close()
pipeout.close(); pipeerr.close()
3.1.3 Start the Flask application
"startFlask.sh" 3.10 =LOGDIR='/home/ajh/logs/central'
HOUSE='/home/ajh/Computers/House'
if [ -f ${LOGDIR}/flaskProcess ] ; then
for p in `cat ${LOGDIR}/flaskProcess` ; do
kill -9 $p
done
rm ${LOGDIR}/flaskProcess
fi
${HOUSE}/lilydaleFlask.py >>${LOGDIR}/flask.log 2>&1 &
ps aux | grep "Flask.py" | grep -v grep | awk '{print $2}' >${LOGDIR}/flaskProcess
3.1.4 Watchdog program for Flask
"watchFlask.py" 3.11 =
<edit warning 2.1>
time=`/home/ajh/bin/date`
curl -s -m 10 http://lilydale.home:5000/house >/dev/null
status=$?
if [ $status -ne 0 ] ; then
echo "$time status is $status, restarting"
restart flask
else
echo "$time status is $status, OK"
fi
The flask application seems rather unreliable, and keeps
freezing. In an effort to avoid this causing glitches to
the access of the HouseMade system, this watchdog program
has been implemented. Its operation is to request a
download of the house web page, and if this times out, then
a restart flask is issued. Essential to its correct
operation is that it must be run as root, so that the
restart flask operation runs correctly. Hence it
must have its sudo sticky bit set, viz sudo chmod 4711
watchFlask.sh - a reminder of this is given by the
Makefile (q.v.).
Note that the status returned by curl must be
captured, as it is used several times subsequently, and the
$? variable gets overwritten.
3.2 The HouseMade module
"HouseMade.sh" 3.12 =#!/usr/bin/python
## H o u s e M a d e . p y
##
## **********************************************************
## * do NOT EDIT THIS FILE! *
## * Use $HOME/Computers/House/HouseMade.xlp instead *
## **********************************************************
import datetime
import cgi,math,string
import re
import time
import xmlrpclib
import ChookDoor
import currentState
import wx200
from HouseDefinitions import *
WServer='http://%s:5000/weather' % (CENTRAL)
<HouseMade: define the Generate Weather Data routine 3.13>
<HouseMade: define the Generate Solar Data routine 3.14>
<HouseMade: define the Generate Tank Data section 3.15>
def
house(remadr,server,args):
import os,sys
DEBUG=False
SServer='http://%s:5000/solar' % (CENTRAL)
TServer='http://%s:5000/tank' % (CENTRAL)
WServer='http://%s:5000/weather' % (CENTRAL)
<HouseMade: collect date and time data 3.16>
picserver='wolseley'
HServer='http://%s:5000/heating' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
<HouseMade: check client connection 3.17>
(aimtemp,onoff) = (ThermostatSetting,'off') #garedelyon.getHeating()
res=0
onoffcolor='blue'
# legacy code - will be reinstated some day
if onoff=='on': onoffcolor='red'
sys.path.append('/home/ajh/Computers/House/')
# now respond to any argument requests - can only do if RPC server present
active=False
if args and CentralGood:
currState=Central.getState()
for relay in RelayNames:
if args.has_key(relay):
active=True
bitNo=RelayTable[relay]
newState=args[relay][0]
if newState in ['off','on']:
doWhat={'off':Central.setBitOff,'on':Central.setBitOn}
change=doWhat[newState](bitNo)
print "change bit %d(%s) to %s" % (bitNo,relay,newState)
else:
# start timer with time == newstate
timerCount=int(newState)
Central.start(bitNo,timerCount)
print "timer started for bit %d(%s) for %d" % \
(bitNo,relay,timerCount)
now=datetime.datetime.now()
jobtime=str(now-starttime)
maxmintab={}#garedelyon.maxminTemp()
now=datetime.datetime.now()
jobtime=str(now-starttime)
import ChookDoor
chooks=ChookDoor.ChookDoor()
chooks.load()
localrise=chooks.sunrise
localset=chooks.sunset
gateopentime=chooks.dooropen
gateshuttime=chooks.doorshut
suns = '''
<table>
<tr>
<td align="right">Today sunrise is at </td><td style="color:#e04000"> %s </td>
<td align="right">and sunset is at </td><td style="color:#e04000"> %s </td>
</tr>
<tr>
<td>The chook gate opening time is </td><td style="color:#c04000"> %s </td>
<td> and the shutting time is </td><td style="color:#c04000"> %s </td>
</tr>
</table>
''' % (localrise,localset,gateopentime,gateshuttime)
import os,string
# determine what relays are currently switched on
if CentralGood:
state=Central.getState()
currentcircuits=[]
for i in range(NumberOfRelays):
if state[i]==1:
currentcircuits.append(RelayNames[i])
if len(currentcircuits) > 1:
currentcircuits = "the " + string.join(currentcircuits[:-1], ", ") + " and " + currentcircuits[-1]
elif len(currentcircuits) > 0:
currentcircuits = "the " + currentcircuits[0]
else:
currentcircuits = "no"
relayState='''
<p>
Currently %(currentcircuits)s circuits are on.
Visit the <a href="%(HServer)s">Heating Timer</a> page;
<a href="%(WServer)s">Weather</a>;
<a href="%(SServer)s">Solar Power</a>;
<a href="%(TServer)s">Tank Storage</a>.
</p>
''' % vars()
else:
relayState='''
<p>
Currently no relay information is available -
have you started the relay server?
</p>
'''
###################
# make the adjust temperature button panel
# this is dynamically constructed to show the current aiming temperature.
buttonColours=['blue','#10e','#20d','#40b','#609','#807',
'#a05','#b04','#c03','#d02','#e01','red']
aimIndex=math.trunc(aimtemp+0.5)-12
if aimtemp<=12.5: aimIndex=0
elif aimtemp>=22.5: aimIndex=11
buttonColours[aimIndex]='yellow'
adjustPanel='''
<td><button name="button" value="" type="submit"></button></td>
<td bgcolor="%s">
<button name="button" value="cooler" type="submit">COOLER</button>
</td>
<td bgcolor="%s"><button name="button" value="13" type="submit">13C</button></td>
<td bgcolor="%s"><button name="button" value="14" type="submit">14C</button></td>
<td bgcolor="%s"><button name="button" value="15" type="submit">15C</button></td>
<td bgcolor="%s"><button name="button" value="16" type="submit">16C</button></td>
<td bgcolor="%s"><button name="button" value="17" type="submit">17C</button></td>
<td bgcolor="%s"><button name="button" value="18" type="submit">18C</button></td>
<td bgcolor="%s"><button name="button" value="19" type="submit">19C</button></td>
<td bgcolor="%s"><button name="button" value="20" type="submit">20C</button></td>
<td bgcolor="%s"><button name="button" value="21" type="submit">21C</button></td>
<td bgcolor="%s"><button name="button" value="22" type="submit">22C</button></td>
<td bgcolor="%s">
<button name="button" value="hotter" type="submit">HOTTER</button>
</td>
''' % (tuple(buttonColours))
################### Relay Control
if CentralGood:
row="<tr><th>Name</th><th>bit No</th><th>On/Off</th>"
row+="<th>Timer</th><th colspan='8'>Run For</th></tr>"
for key in sorted(RelayTable, key=RelayTable.get):
bitNo=RelayTable[key]
thisBit=Central.getState()[bitNo]
thisState=['off','on'][thisBit]
newState=['on','off'][thisBit]
thisColour=['lightblue','red'][thisBit]
row += '<tr bgcolor="%s">\n' % (thisColour)
row += ' <td>%s</td>' % (key)
row += ' <td bgcolor="%s">%s</td>' % (thisColour,bitNo) # bit number
row += ' <td bgcolor="%s">' % (thisColour)
row += '<button name="%s" value="%s" type="submit">' % (key,newState)
row += '%s</td>' % (thisState)
timeLeft=Central.getTimer(bitNo)
if timeLeft>0: active=True
row += ' <td bgcolor="%s" width="50px">%d</td>' % (thisColour,timeLeft)
for t in [30,60,120,300,600,1200,1800,3600]:
if t<60:
buttontxt="%d secs" % (t)
elif t>=3600:
buttontxt="%d hour" % (t/3600)
else:
buttontxt="%d min" % (t/60)
t1=t
if key=='FloodNDrain' and t>60:
t1=60; buttontxt="1 min"
row += ' <td><button name="%s" value="%d" type="submit">%s</td>' % (key,t1,buttontxt)
row += "</tr>\n"
################### Chook Door
house=currentState.HouseState()
house.load()
chookdoorstate=house.get('chookDoorProve') # was chooks.current
if chookdoorstate == 'closed':
chookdoorcolour="green"
else:
chookdoorcolour="red"
row += '<tr bgcolor="%s">\n' % (chookdoorcolour)
row += ' <td>ChookDoor</td>'
row += '<td colspan="11" align="center">%s</td>\n' % (chookdoorstate)
row += '</tr>'
###################
relaycontrol="""
<form action="%(MServer)s" method="get" name="runRelay">
<table border="1">
%(row)s
</table>
</form>
%(relayState)s
""" % vars()
else:
relaycontrol='<p>No relay information available</p>'
################### Water Storage
tanksection=tank([])
################### Solar Power
solarsection=solar([])
################### Climate
weathersection=weather([])
###################
if active:
redirect="10;URL='%s'" % (MServer)
else:
redirect="60"
housepage=
<HouseMade: generate the web page content 3.18>
return housepage
if __name__=='__main__':
house()
##
## The End
##
3.2.1 define the Generate Weather Data routine
<HouseMade: define the Generate Weather Data routine 3.13> =def weather(args):
WServer='http://%s:5000/weather' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
MAXMINFILE='/home/ajh/logs/central/maxmintemps.log'
w=wx200.weather()
curtemp = w.inside.temp
curhumid = w.inside.humidity
outtemp = w.outside.temp
outhumid = w.outside.humidity
starttime=datetime.datetime.now()
yesterday=starttime-datetime.timedelta(days=1)
yesterday=yesterday.strftime("%Y%m%d")
today=starttime.strftime("%Y%m%d")
# check maximum and minimum
maxminpat='(\d\d\d\d\d\d\d\d)' # date only
maxminpat+=' +([0-9.]+)' # maximum temp
maxminpat+=' +([0-9:]+)' # maximum temp time
maxminpat+=' +([0-9.]+)' # minimum temp
maxminpat+=' +([0-9:]+)' # minimum temp time
maxminpat=re.compile(maxminpat)
f=open(MAXMINFILE,'r')
maxmintable={}
for l in f.readlines():
#print "read maxmin line of %s" % (l)
res=maxminpat.match(l)
if res:
d=res.group(1)
max=float(res.group(2))
maxat=res.group(3)
min=float(res.group(4))
minat=res.group(5)
maxmintable[d]=(max,maxat,min,minat)
else:
print "cannot parse %s" % (l)
f.close()
if maxmintable.has_key(yesterday):
(yestermax,yestermaxat,yestermin,yesterminat)=maxmintable[yesterday]
else:
print "<P>Min/Max temperatures not available for yesterday</P>\n"
(yestermax,yestermaxat,yestermin,yesterminat)=(0.0, '00:00', 0.0, '00:00')
if maxmintable.has_key(today):
(max,maxat,min,minat)=maxmintable[today]
else:
print "<P>Min/Max temperatures not available for today</P>\n"
(max,maxat,min,minat)=(0.0, '00:00', 0.0, '00:00')
# get desired temperature
house=currentState.HouseState()
house.load()
aimtemp=house.get('thermostat')
onoffcolor="black"
weathersection="""
<h2><a href="%(WServer)s">Weather</a></h2>
<image src="personal/tempplot.png"/>
<form method="POST" action="house.py">
<p>
Outside it is %(outtemp).1fC and %(outhumid)d%% humid.
It is currently %(curtemp).1fC and %(curhumid)d%% humid inside.
<span style="color:%(onoffcolor)s">The heating is aiming for
<input type="text" size="6" name="temp" value="%(aimtemp).1f"></input>
C.</span>
</p>
</form>
<p>
Temperature Extremes on the outside:
<table align="center" width="80%%">
<tr><th>Yesterday</th><th>Today</th></tr>
<tr>
<td align="center">maximum=%(yestermax)s at %(yestermaxat)s</td>
<td align="center">maximum=%(max)s at %(maxat)s</td>
</tr>
<tr>
<td align="center">minimum=%(yestermin)s at %(yesterminat)s</td>
<td align="center">minimum=%(min)s at %(minat)s</td>
</tr>
</table>
</p>
<p><a href="%(MServer)s">Back to House</a></p>
""" % vars()
return weathersection
This routine collects up the climate/temperature/heating
data and builds a web page to display it. The web page is
returned as a string, allowing it to be directly called by
the Flask module.
The maxminTemp() call will return an empty
dictionary if it cannot find the maximum and minimum
temperatures, so we need key ckecks to avoid run time
errors.
3.2.2 define the Generate Solar Data routine
<HouseMade: define the Generate Solar Data routine 3.14> =def solar(args):
SServer='http://%s:5000/solar' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
in_Ah = Central.getSolar(20)
out_Ah = Central.getSolar(24)
solaramps = Central.getSolar(32)
solarbatteryvolts = Central.getSolar(34)*0.1+15
solarpower = solaramps*solarbatteryvolts
percentsolar = solaramps * 100.0 / 50.0
in_whr = int(in_Ah*27.6)
in_MJ = (in_Ah*27.6*3.6/1000)
solarsection="""
<h2><a href="%(SServer)s">Solar power</a></h2>
<image src="personal/solarplot.png"/>
<p>
We are getting %(solaramps).1fA into our %(solarbatteryvolts).1fV
batteries for a total power output of %(solarpower).1fW, or
about %(percentsolar).1f%% of maximum rated power.<br/>
Today we've had %(in_Ah)dAh in (%(in_whr)dWhr=%(in_MJ).1fMJ) and
%(out_Ah)dAh out. Note that the ampere-hours out is quoted for the
24vdc level, not the 240vac being generated by the inverter.<br/>
There is a <a href="solar.py">detailed log</a> available, and the
<a href="https://engage.efergy.com/dashboard" target="_blank">
Engage platform</a>
gives real time power consumption.
</p>
<p><a href="%(MServer)s">Back to House</a></p>
""" % vars()
return solarsection
Collect the solar power information for display and
generate the related text.
3.2.3 define the Generate Tank Data routine
<HouseMade: define the Generate Tank Data section 3.15> =def tank(args):
TServer='http://%s:5000/tank' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
# get tank data
tankvolume=Central.getTank()
tanktemp=volts=0.0
tankfull=4500
tankpercent = (tankvolume/float(tankfull))*100
tanksection="""
<h2><a href="%(TServer)s">Tank Storage</a></h2>
<image src="personal/tankplot.png"/>
<p>
The rain water tank is currently at %(tankvolume).1fl(/%(tankfull)dl) =
%(tankpercent).1f%%. Check the 7 day graph:
</p>
<image src="personal/tankplot7.png"/>
<p><a href="%(MServer)s">Back to House</a></p>
""" % vars()
return tanksection
This section now installed on lilydale.
3.2.4 Collect Date and Time Data
<HouseMade: collect date and time data 3.16> =# collect date and time data
(year, month, day, hour, minute, second,
weekday, yday, DST) = time.localtime(time.time())
#tm = time.asctime(time.localtime(time.time())) + \
# ["", "(Daylight savings)"][DST]
tm=datetime.datetime.now()
tm=tm.strftime("%a, %d %b %Y, %H:%M:%S")
starttime=datetime.datetime.now()
now=datetime.datetime.now()
jobtime=str(now-starttime)
# isoweek is Mon-Sun, 1-7, but want 0-origin starting with Sun
# Sun Mon Tue Wed Thu Fri Sat
# iso: 7 1 2 3 4 5 6
# 0-org 0 1 2 3 4 5 6
weekday=now.isoweekday() % 7
The date and time at which this program is run is useful
for logging, so collect it at the start of operations. The
variable jobtime is intended to check how intensive
use of this code may become.
3.2.5 Check the Client Connection
<HouseMade: check client connection 3.17> =if os.environ.has_key('SSH_CONNECTION'):
clientIP=os.environ['SSH_CONNECTION']
res=re.match('^(\d+\.\d+\.\d+\.\d+).*$',clientIP)
if res:
clientID=res.group(1)
else:
clientIP='255.255.255.0'
#print os.environ
print clientIP
res=re.match('10\.0',clientIP)
if res:
clientOK=True
else:
res=re.match('130\.194\.69\.41',clientIP)
if res:
clientOK=True
else:
clientOK=False
sys.stderr.write("ATTEMPT TO ALTER HOUSE SETTINGS\n")
This page is intended to be world-wide-web accessible, and
hence we must establish the credentials of the client. If it
is on the local network, no problem, but external users must
authenticate (username/passwd) before being allowed to alter
any parameters.
The IP address is used in the first instance, as given by
the environment variable 'SSH_CONNECTION'. If we can
extract the 4-block IP address, well and good, otherwise
make it the local mask. IP addresses on the local network
(10.0.0.*) are allowed, as is the IP address of my work
computer. All others pay money, and their names are taken.
3.2.6 Generate the Web Page Content
<HouseMade: generate the web page content 3.18> ="""
<HTML>
<HEAD>
<LINK REL="SHORTCUT ICON" HREF="favicon.ico">
<meta http-equiv="Refresh" content="%(redirect)s">
<meta http-equiv="Pragma" content="no-cache">
<TITLE>HouseMade</TITLE>
</HEAD>
<BODY>
<h1>
<a href="%(MServer)s">
HouseMade: the Hurst House Heater Helpmate
</a>
</h1>
HouseMade thinks it is currently %(tm)s.
You might want to see what rain is <a
href="http://mirror.bom.gov.au/products/IDR023.shtml">
happening in melbourne</a>, or the
<a href="http://www.bom.gov.au/vic/forecasts/scoresby.shtml">
local forecast</a>.
%(suns)s
%(relayState)s
%(relaycontrol)s
%(weathersection)s
%(solarsection)s
%(tanksection)s
</BODY>
</HTML>
""" % vars()
This is where the framework of the web page is generated.
Most of the content is generated elsewhere as strings to be
inserted into this template, hence the global dictionary
call on vars at the end, with variable content being
added via %s formatting imperatives.
3.2.7 Make the Temperature Panel (not currently used)
<house make temperature panel 3.19> =# make the adjust temperature button panel
# this is dynamically constructed to show the current aiming temperature.
buttonColours=['blue','#10e','#20d','#40b','#609','#807',
'#a05','#b04','#c03','#d02','#e01','red']
aimIndex=math.trunc(aimtemp+0.5)-12
if aimtemp<=12.5: aimIndex=0
elif aimtemp>=22.5: aimIndex=11
buttonColours[aimIndex]='yellow'
adjustPanel='''
<td><button name="button" value="" type="submit"></button></td>
<td bgcolor="%s">
<button name="button" value="cooler" type="submit">COOLER</button>
</td>
<td bgcolor="%s"><button name="button" value="13" type="submit">13C</button></td>
<td bgcolor="%s"><button name="button" value="14" type="submit">14C</button></td>
<td bgcolor="%s"><button name="button" value="15" type="submit">15C</button></td>
<td bgcolor="%s"><button name="button" value="16" type="submit">16C</button></td>
<td bgcolor="%s"><button name="button" value="17" type="submit">17C</button></td>
<td bgcolor="%s"><button name="button" value="18" type="submit">18C</button></td>
<td bgcolor="%s"><button name="button" value="19" type="submit">19C</button></td>
<td bgcolor="%s"><button name="button" value="20" type="submit">20C</button></td>
<td bgcolor="%s"><button name="button" value="21" type="submit">21C</button></td>
<td bgcolor="%s"><button name="button" value="22" type="submit">22C</button></td>
<td bgcolor="%s">
<button name="button" value="hotter" type="submit">HOTTER</button>
</td>
''' % (tuple(buttonColours))
This code is separated out because of the complexity of
loading the colours of each of the demand buttons. Each
button gets a graduated colour from blue through to red,
except for the currently specified temperature, which is shown
with a yellow background. Temperatures are rounded to the
nearest integer in order to determine which button is so
highlighted. Temperatures above and below the selectable
range highlight the HOTTER and COOLER buttons respectively.
There is a slight glitch with the operation of this form, in
that when no specific temperature button is selected (such as
when a text value of temperature is entered, the first button
is selected (which would normally be COOLER). (See <house get calling parameters >.) To avoid this, a
dummy blank button (value="") is built in at the start
of the table list, and when this blank value is recognized,
the text value is used instead.
3.3 The HeatingModule module
This web page provides a user-friendly interace to setting
the automatic heating on/off times, and the temperatures over
the course of a day (this latter function not yet
operational). The week is divided into 7 days, each with its
own programme.
Each day can have up to 7 blocks of time, where the start
time of the first block is midnight (00:00 hours), and the end
time of the last block is the next midnight (24:00 hours).
The end time of each block can be altered, and the number of
blocks is determined by the block that has end time of
24:00.
On saving, if the last end time is earlier than 24:00, a new
block is added (up to 5 blocks). If seven blocks are in use,
and the last end time does not end at midnight, the program
will be incomplete and the actual behaviour is not
defined.
The desired temperature of each time block can be set
independently.
"HeatingModule.py" 3.20 =#! /usr/bin/python
## **********************************************************
## * do NOT EDIT THIS FILE! *
## * Use $HOME/Computers/House/HouseMade.xlp instead *
## **********************************************************
##
## 20141113:114917 1.0.0 ajh first version with number
## 20141113:114958 1.0.1 ajh elide start times if narrow column
## 20150722:164226 1.1.0 ajh copied from TimerModule and updated
##
import cgi,datetime,math,os,sys,re,time
from HouseDefinitions import *
DEBUG=False
<Web: define the heatingData class 3.21>
def
heating(logMsg,remadr,args):
DEBUG=False
active=False
<HouseMade: collect date and time data 3.16>
environ=os.environ
if DEBUG:
keys=environ.keys()
keys.sort()
print "environ:"
for key in keys:
print " %s:%s" % (key,environ[key])
if DEBUG:
keys=args.keys()
keys.sort()
print "arguments",
lastKey=''
for key in keys:
if key[0:3]!=lastKey:
print "\n ",
lastKey=key[0:3]
print "%s:%s" % (key,args[key]),
print "\n\n", # put 2 newlines at end
server='Central'
#print "server=%s" % (server)
clientIP=remadr
res=re.match('10\.0',clientIP)
if res:
clientOK=True
else:
res=re.match('130\.194\.69\.41',clientIP)
if res:
clientOK=True
else:
clientOK=False
logMsg("clientOK=%s (%s)" % (clientOK,clientIP))
# create data structures and initialize
td=heatingData()
# load previously saved data
td.load('/home/ajh/Computers/House/heatProgram.dat')
<Web: heating: collect parameters and update 3.22>
<Web: heating: build widths for web page table 3.23>
# build web page
redirect=''
if active:
redirect='''<meta http-equiv="Refresh" content="10;URL='%s'>''' % (HServer)
out="<HTML>\n<HEAD>\n"
out+=redirect
out+='<meta http-equiv="Pragma" content="no-cache">\n'
out+='<TITLE>HeatingTimer</TITLE>\n'
out+="weekday=%d, now=%s" % (weekday,now)
if not clientOK:
out += "<P>Sorry, you are not authorized to adjust this table</P>"
else:
out += '<form action="%s" method="get" name="heating">\n' % (HServer)
out += ' <button name="button" value="save">save</button>\n'
out += ' <table border="1" width="100%" padding="0">\n'
for i in range(7):
out += " <tr height='40px'>\n"
if i==weekday:
dayColour="#8f8"
else:
dayColour="#fff"
out += " <td width='10%%' bgcolor='%s'>%s</td>\n" % (dayColour,td.days[i])
out += " <td><table width='100%' height='100%' border='0' padding='0' cellspacing='0'><tr>\n"
for j in range(NTempBlocks):
if td.width[i][j]==0: continue
out += " <td bgcolor='%s' width='%d%%' height='35px' align='center'>\n" % (td.colour[i][j],td.width[i][j])
out += ' <select name="temp-%d-%d" size="1">\n' % (i,j)
for k in range(10,27):
selected=""
if k==td.temp[i][j]:
selected="selected"
out += ' <option value="%d" %s>%d</option>\n' % (k,selected,k)
out += ' </select>\n'
out += " </td>\n"
out += " <td> </td>\n"
out += " </tr>\n"
out += " <tr>\n"
for j in range(NTempBlocks):
if td.width[i][j]==0: continue
out += " <td bgcolor='%s' width='%d%%' height='35px' align='center'>\n" % (td.colour[i][j],td.width[i][j])
out += " <table border='0'>\n"
(sh,sm)=td.mins2Hours(td.start[i][j])
(eh,em)=td.mins2Hours(td.end[i][j])
if td.width[i][j]>20:
out += ' <tr><th>Start</th><th>End</th></tr>\n'
out += ' <tr><td>%02d:%02d</td>\n' % (sh,sm)
else:
out += ' <tr><th>End</th></tr>\n'
out += ' <tr>\n'
out += ' <td>\n'
out += ' <select name="end-%d-%d" size="1">\n' % (i,j)
for k in range(0,25):
selected=""
if k==eh:
selected="selected"
out += ' <option value="%02d" %s>%02d</option>\n' % (k,selected,k)
out += ' </select>\n'
out += ' <select name="endmin-%d-%d" size="1">\n' % (i,j)
for k in range(0,12):
selected=""
if 5*k==em:
selected="selected"
out += ' <option value="%02d" %s>%02d</option>\n' % (5*k,selected,5*k)
out += ' </select>\n'
out += ' </td>\n'
out += ' </tr>\n'
out += ' <tr>\n'
out += ' <td>\n'
out += ' </td>\n'
out += ' </tr>\n'
out += " </table>\n"
out += " </td>\n"
out += " <td> </td>\n"
out += " </tr></table></td>\n"
out += " </tr>\n"
out += " </table>\n"
out += '</form>\n'
out += '<A HREF="http://%s:5000/house">back to house</A>\n' % (CENTRAL)
if clientOK:
td.save('/home/ajh/Computers/House/heatProgram.dat')
print "--------"
return out
if __name__=='__main__':
heating()
3.3.1 Define the heatingData Class
<Web: define the heatingData class 3.21> =class heatingData():
def __init__(self):
self.days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
self.temp=[[ThermostatSetting for j in range(NTempBlocks)] for i in range(7)]
self.start=[[0 for j in range(NTempBlocks)] for i in range(7)]
self.end=[[0 for j in range(NTempBlocks)] for i in range(7)]
self.width=[[10 for j in range(NTempBlocks)] for i in range(7)]
self.colour=[['red' for i in range(NTempBlocks)] for j in range(7)]
def mins2Hours(self,m):
return (m/60,m%60)
def hours2Mins(self,h,m):
return 60*h+m
def load(self,filename='/home/ajh/Computers/House/heatProgram.dat'):
f=open(filename,'r')
for i in range(7):
day=f.readline()
res=re.match('Day (\d)$',day)
if res:
rd=int(res.group(1))
if rd!=i:
print "Could not read data at day %s" % (i)
for j in range(NTempBlocks):
block=f.readline()
res=re.match('(\d) (\d\d)(\d\d)-(\d\d)(\d\d):(\d\d)$',block)
if res:
n=int(res.group(1))
s=60*int(res.group(2))+int(res.group(3))
e=60*int(res.group(4))+int(res.group(5))
t=int(res.group(6))
self.temp[i][j]=t
self.start[i][j]=s
self.end[i][j]=e
self.colour[i][j]=setColour(self.temp[i][j])
if n!=j:
print "Error on block %d on day %d" % (j,i)
else:
break;
if block.strip()!='':
blank=f.readline()
f.close()
def save(self,filename='/home/ajh/Computers/House/heatProgram.dat'):
f=open(filename,'w')
for i in range(7):
f.write("Day %d\n" % (i))
for j in range(NTempBlocks):
s=self.start[i][j]
sh=s/60; sm=s%60
e=self.end[i][j]
eh=e/60; em=e%60
t=self.temp[i][j]
f.write("%1d %02d%02d-%02d%02d:%02d\n" % (j,sh,sm,eh,em,t))
f.write("\n")
pass
f.close()
This class deals with all the logic needed to load and save
the heating data, stored in a separate file. It handles
conversion from external stored time data in hours:minutes
format, converting it to internally stored minutes only (from
the start of the day), and reconverting it back again on
saving.
It also provides a few simple conversion routines for
switching between the formats.
3.3.2 Collect Parameters and Update
<Web: heating: collect parameters and update 3.22> =# collect parameters
if clientOK:
if args:
active='True'
keys=args.keys()
keys.sort()
for k in keys:
#print "k=%s" % (k)
res=re.match('(temp|start|end|endmin)-(\d+)-(\d+)',k)
if res:
t=res.group(1)
d=int(res.group(2))
b=int(res.group(3))
#print "got type=%s, day=%d, block=%d" % (t,d,b)
if t=='temp':
tt=args[k][0]
#print "d=%s, b=%s, temp=%s, args[%s]=%s" % (d,b,tt,k,args[k])
t=setTemperature(args[k][0])
td.temp[d][b]=t
td.colour[d][b]=setColour(t)
pass
# 'start' is never used
#if t=='start':
# #print "<p>",temp,k,args[k]
# start[d][b]=int(args[k][0])
# pass
if t=='end':
#print "<p>",temp,k,args[k]
(h,m)=td.mins2Hours(td.end[d][b])
td.end[d][b]=td.hours2Mins(int(args[k][0]),m)
pass
if t=='endmin':
#print "<p>",temp,k,args[k]
(h,m)=td.mins2Hours(td.end[d][b])
if h>=24:
m=0
else:
m=int(args[k][0])
td.end[d][b]=td.hours2Mins(h,m)
pass
3.3.3 Build Widths for Web Page Table
<Web: heating: build widths for web page table 3.23> =# build widths for table
for i in range(7):
dayFinished=False
for j in range(NTempBlocks):
if j>0:
# make unused blocks alternate in temperature
if td.start[i][j]==1440: # 1440 is midnight, hence unused
if td.temp[i][j-1]==10:
td.temp[i][j]=ThermostatSetting
else:
td.temp[i][j]=10
try:
td.start[i][j]=td.end[i][j-1]
except IndexError:
print "index error in HeatingModule: i=%d, j=%d (start=%s, end=%s)" \
% (i,j,td.start,td.end)
if td.start[i][j]>td.end[i][j]:
td.end[i][j]=60*24
w=td.end[i][j]-td.start[i][j]
if w<0: w=0
if dayFinished: w=0
td.width[i][j]=math.trunc(100*w/1440.0) # percentage width
(h,m)=td.mins2Hours(td.end[i][j])
if h==24 or h==0:
dayFinished=True
#print "got day finished at day=%d, block=%d" % (i,j)
pass
<check client IP address OK 3.24> =if os.environ.has_key('REMOTE_ADDR'):
clientIP=os.environ['REMOTE_ADDR']
else:
clientIP='255.255.255.255'
print os.environ
res=re.match('10\.0',clientIP)
if res:
clientOK=True
else:
res=re.match('130\.194\.69\.41',clientIP)
if res:
clientOK=True
else:
clientOK=False
This code fragment checks to see if the client IP address is on
the local network, and sets clientOK to True if it
is, False otherwise.
4. The Weather System
The heart of the weather system is an electronic weather
station with RS232 output, that is continously monitored by the
garedelyon server. Once a minute, this server logs the
current inside and outside temperatures, humidity and dew points.
This information is stored in a logfile, temp.log in the
directory /logdisk/logs/, and accessed by the heating and
web systems.
4.1 The C Weather Monitor Program
(Details to be recorded)
4.2 The Python Interface to the Weather System
This is a simple python module that accesses the monitor
progam and makes the data accessible as python structures.
There is one substantive class, weather, instances of
which entities containing the appropriate data.
The null classes environment, wind,
rain, and pressure provide further localization
of the data values.
- environment
- defines
values for temperature, humidity, and dew
point;
- wind
- defines values for wind gust, wind
gustdirirection, avgerage wind speed,
avgdir average wind direction, and wind
chill factor.
"wx200.py" 4.1 =#!/usr/bin/python
<edit warning 2.1>
import os,string,sys,subprocess
class environment: pass
class wind: pass
class rain: pass
class pressure: pass
class weather:
def __init__(self, hostname = "10.0.0.112"):
host=os.getenv('HOST')
pgm='/home/ajh/Computers/House/wx200d-1.1/wx200 '
opts='--power --battery --display -a --C --kph --hpa --mm --mm/h'
cmd="%s %s -l %s --nounits" % (pgm,hostname,opts)
f = os.popen(cmd)
line = f.readline()
l = map(lambda s:string.strip(s[:-1]), string.split(line, "\t"))
self.inside = environment()
self.outside = environment()
self.inside.temp = float(l[0])
self.inside.humidity = int(l[2])
self.inside.dew = int(l[4])
self.outside.temp = float(l[1])
self.outside.humidity = int(l[3])
self.outside.dew = int(l[5])
self.pressure = pressure()
self.pressure.local = int(l[6])
self.pressure.sea = int(l[7])
self.wind = wind()
self.wind.gust = float(l[8])
self.wind.gustdir = int(l[9])
self.wind.avg = float(l[10])
self.wind.avgdir = int(l[11])
self.wind.chill = int(l[12])
self.rain = rain()
self.rain.rate = int(l[13])
self.rain.daily = int(l[14])
self.rain.total = int(l[15])
if __name__ == '__main__':
d = weather()
for n in dir(d):
a = getattr(d, n)
for nn in dir(a):
print "%s.%s." % (n, nn), str(getattr(a, nn))
4.3 The Weather Logging Process
This code is run once a minute by the EveryMinute.sh
script, and simply outputs the inside temperature, the outside
temperature, the inside humidity, the outside humidity, and the
outside dew point data to the log file.
"logwx.py" 4.2 =import datetime
import re
from wx200 import *
STATEFILE='/home/ajh/logs/central/wxState.txt'
MAXMINFILE='/home/ajh/logs/central/maxmintemps.log'
w=weather()
inside=w.inside
intemp=inside.temp
outside=w.outside
wind=w.wind
rain=w.rain
now=datetime.datetime.now()
nowstamp=now.strftime("%Y%m%d:%H%M%S")
if intemp==0.0:
f=open(STATEFILE,'r')
prev=f.readline()
res=re.match('\d{8}:\d{6} +(\d+\.\d) +(\d+\.\d)',prev)
if res:
intemp=float(res.group(1))
outside.temp=float(res.group(2))
else:
print "Could not match %s" % (prev)
line="%s %5.1f %5.1f" % (nowstamp,intemp,outside.temp)
line+=" %5.1f %5.1f" % (inside.humidity,outside.humidity)
line+=" %5.1f" % (outside.dew)
line+=" %5.1f %5.1f" % (wind.gust,wind.gustdir)
line+=" %5.1f %5.1f" % (rain.rate,rain.daily)
print line
# save current state
f=open(STATEFILE,'w')
f.write(line+'\n')
f.close()
# check maximum and minimum
maxminpat='(\d\d\d\d\d\d\d\d)' # date only
maxminpat+=' +([0-9.]+)' # maximum temp
maxminpat+=' +([0-9:]+)' # maximum temp time
maxminpat+=' +([0-9.]+)' # minimum temp
maxminpat+=' +([0-9:]+)' # minimum temp time
maxminpat=re.compile(maxminpat)
f=open(MAXMINFILE,'r')
maxmintable={}
for l in f.readlines():
#print "read maxmin line of %s" % (l)
res=maxminpat.match(l)
if res:
d=res.group(1)
max=float(res.group(2))
maxat=res.group(3)
min=float(res.group(4))
minat=res.group(5)
maxmintable[d]=(max,maxat,min,minat)
else:
print "cannot parse %s" % (l)
f.close()
changed=False
today=now.strftime("%Y%m%d")
time=now.strftime("%H:%M")
if maxmintable.has_key(today):
(max,maxat,min,minat)=maxmintable[today]
else:
maxmintable[today]=(outside.temp,time,outside.temp,time)
changed=True
(max,maxat,min,minat)=maxmintable[today]
if outside.temp>max:
max=outside.temp
maxat=time
changed=True
elif outside.temp<min:
min=outside.temp
minat=time
changed=True
if changed:
maxmintable[today]=(max,maxat,min,minat)
f=open(MAXMINFILE,'w')
keys=maxmintable.keys()
keys.sort()
for k in keys:
(max,maxat,min,minat)=maxmintable[k]
f.write("%s %5.1f %s %5.1f %s\n" % (k,max,maxat,min,minat))
#print "%s %5.1f %s %5.1f %s" % (k,max,maxat,min,minat)
f.close()
5. The Heating System
5.1 AdjustHeat
This script runs every minute on lilydale, to see if the
heating should be adjusted. An entry in EveryMinute.sh
invokes this program. It has recently (v1.3.0) been revised
back to the original concept of allowing an arbitrary desired
temperature to be specified, and it records its decisions in
the log file heating.log (which see <EveryMinute.sh 15.1>.
"AdjustHeat.py" 5.1 =
<edit warning 2.1>
# this code must run on lilydale
import datetime
import re
import RelayControl
import xmlrpclib
import HeatingModule
import currentState
from HouseDefinitions import *
import wx200
Debug=False
hysteresis=0.25
now=datetime.datetime.now()
nowStamp=now.strftime("%Y%m%d:%H%M%S")
dayofweek=now.isoweekday() % 7
hd=HeatingModule.heatingData()
hd.load('/home/ajh/Computers/House/heatProgram.dat')
wx=wx200.weather()
currentTemp=wx.inside.temp
# check if time to change temp
t=hd.temp[dayofweek]
s=hd.start[dayofweek]
e=hd.end[dayofweek]
if Debug: print "t=%s, s=%s" % (t,s)
hour=now.hour; min=now.minute
hourMin=60*hour+min
switch=0; ptemp=0
for i in range(len(t)):
smins=s[i]; emins=e[i]
if Debug: print smins,hourMin,emins
if smins<=hourMin and hourMin<emins:
switch=emins
ptemp=t[i]
break
desiredTemp=ptemp
(swh,swm)=hd.mins2Hours(switch)
if Debug:
print "on day %s at time %s, planned temp=%d, \
desired temp=%s, next switch=%02d%02d" % \
(hd.days[dayofweek],now.strftime("%Y%m%d:%H%M"),ptemp,\
desiredTemp,swh,swm)
# change heating here, but only if need to change!
bitNo=RelayTable['Heating']
state='OK'; change=turn=''
if currentTemp<desiredTemp-hysteresis:
state="low"; turn='on'
elif currentTemp>desiredTemp+hysteresis:
state="high"; turn='off'
if turn:
change=', turn %s' % turn
print "%s AdjustHeat: current=%4.1fC, desired=%dC, heating is %s%s" % (nowStamp,currentTemp,desiredTemp,state,change)
if turn=='on':
# turn heating on here
Central.setBitOn(bitNo)
pass
if turn=='off':
# turn heating off here
Central.setBitOff(bitNo)
pass
# save current state of desired temperature
house=currentState.HouseState()
house.load()
house.store('thermostat',desiredTemp)
house.save()
Note that the variable hysteresis defines how far from
the desired temperature the current temperature must depart
before the heating will change state.
A suggested improvement is to check the current state of the
heating (via the relay controller) to see whether the heating
is currently on or off. If the demanded state is the same as
the current state, then there is no need to explicitly call
for the setBit operation.
5.2 checkTime.py
The responsibility of this program is to periodically
(currently every 5 mins, see cron files) check the programmed
temperature, and update the demand heating file on
garedelyon.
The location of where it runs determines which server is in
control of the heating (currently flinders, but this may
change in future).
"checkTime.py" 5.2 =#! /usr/bin/python
<edit warning 2.1>
import re
import datetime
import xmlrpclib
now=datetime.datetime.now()
dayofweek=now.isoweekday() % 7
time=now.strftime("%H%M")
days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
temp=[[ThermostatSetting for j in range(5)] for i in range(7)]
start=[[0 for j in range(5)] for i in range(7)]
end=[[0 for j in range(5)] for i in range(7)]
# get the current desired temperature
garedelyon=xmlrpclib.ServerProxy('http://garedelyon:8001')
(desiredTemp,onoff) = garedelyon.getHeating()
f=open('/home/ajh/Computers/House/tempProgram.dat','r')
for i in range(7):
day=f.readline()
res=re.match('Day (\d)$',day)
if res:
rd=int(res.group(1))
if rd!=i:
print "Could not read data at day %s" % (i)
for j in range(5):
block=f.readline()
res=re.match('(\d) (\d\d)-(\d\d):(\d\d)$',block)
if res:
n=int(res.group(1))
start[i][j]=int(res.group(2))
end[i][j]=int(res.group(3))
temp[i][j]=int(res.group(4))
if n!=j:
print "Error on block %d on day %d" % (j,i)
blank=f.readline()
f.close()
# check if time to change temp
t=temp[dayofweek]
s=start[dayofweek]
e=end[dayofweek]
print "t=%s, s=%s" % (t,s)
hour=now.hour; min=now.minute
switch=0; ptemp=0
for i in range(len(t)):
st=s[i];en=e[i]
print st,hour,en
if st<=hour and hour<en:
switch=en
ptemp=t[i]
break
if hour in s and min<=1:
desiredTemp=ptemp
print "on day %s at time %s, planned temp=%d, desired temp=%s, next switch=%02d00" % \
(days[dayofweek],now.strftime("%Y%m%d:%H%M"),ptemp,desiredTemp,switch)
res=garedelyon.setHeating(desiredTemp,onoff)
(realtemp,curonoff)=garedelyon.getHeating()
if realtemp!=desiredTemp:
msg="AWOOGA! AWOOGA! someting wrong with g.setHeating()!!!"
msg+="(newdesiredtemp=%s, actualdesiredtemp=%s" % (realtemp,desiredTemp)
print msg
The basic logic of this program is to read the programmed
temperature changes from the file tempProgram.dat (set
by the flinders cgi script heating.py), and adjust the
desired temperature to match the programmed temperature.
The key requirement to this logic is that changes should only
be made at the appointed switch time.
There seems to be some sort of race condition in the
switching logic. The trailing message has been added to try
and identify the circumstances under which the planned
temperature and desired temperature disagree.
5.3 TempLog
This script logs the house temperature. It runs every minute
to maintain a minute-by-minute log. It must be run on
garedelyon, since that is where all logging is collected (the
log file temp.log is kept in the directory
/logdisk/logs).
"TempLog.py" 5.3 =
<edit warning 2.1>
# this code must run on garedelyon
import os,string,datetime
logfile="/logdisk/logs/temp.log"
now=datetime.datetime.now()
nowstr=now.strftime("%Y%m%d:%H%M%S")
log=open(logfile,'a')
hostname='10.0.0.101'
cmd="/home/ajh/Computers/House/wx200d-1.1/wx200 %s -l --nounits" % hostname
f = os.popen(cmd)
line = f.readline()
l = map(lambda s:string.strip(s[:-1]), string.split(line, "\t"))
intemp=float(l[0])
outtemp=float(l[1])
inhumd=float(l[2])
outhumd=float(l[3])
outdew=float(l[5])
currheat=open('/logdisk/logs/heating','r')
ch=currheat.readline()
therm=float(ch)
onoff=currheat.readline().strip()
fmt="%s %5.1f %5.1f %5.1f %5.1f %5.2f %5.1f %s\n"
vars=(nowstr,intemp,outtemp,inhumd,outhumd,outdew,therm,onoff)
log.write(fmt % vars)
6. The Tank System
Define here those program components concerned (solely) with
recording and suppling water tank information.
6.1 Water Tank Logging
The water logging code has been re-written from C to Python,
to bring it in line with the rest of the system, and to
(hopefully) improve the reliability of the logging. The Python
code has been added to the existing code to manage the tank
system, namely tank.py
6.2 Start the Tank Logging
"startTankLog.sh" 6.1 =
<edit warning 2.1>
LOGDIR='/home/ajh/logs/central'
HOUSE='/home/ajh/Computers/House'
USB=`getDevice tank`
kill -9 `cat ${LOGDIR}/tankProcess`
rm ${LOGDIR}/tankProcess
python tank.py $USB &
ps aux | grep "python tank.py" | grep -v grep | awk '{print $2}' \
>>${LOGDIR}/tankProcess
The tank logging is performed slightly differently from the
other logging operations, as the tank level transducer
operates in a free-running open loop mode. Approximately once
a second it sends a burst of data down its RS232 connection,
and so it is necessary to have the logging program running
constantly to hear those data bursts. This is the purpose of
the tank.py program.
The rest of this code is concerned with logging the process
ID of the logging program itself, so that it can be started
and stopped reliably, without interference from any previous
instance.
6.3 Tank Module Functions
The tank module tank.py incorporates some significant
legacy code from Nathan's original Nautilus system design. In
particular, the read_tank_state and readraw are
Python transliterations from Nathan's C code. It is planned
to rewrite this module in the near future using a more
object-oriented approach.
The code now handles the logging function directly.
When called as a main program, it enters an infinite loop,
reading and logging the tank transponder, and output a log
message once a minute.
When used as an imported module, tank.py defines a
number of constants, and provides
functions to access tank data and perform temperature
compensation (see compensate).
Note that this code is under review, and will be cleaned up
in the near future.
"tank.py" 6.2 =#!/usr/bin/python
<edit warning 2.1>
import datetime
import getopt
import re
import string
import sys
import time
import os
import usbFind
LOGFILENAME="/home/ajh/logs/central/tank.log"
maxlitres = 2250 # for one tank
minlitres = 0 # actual somewhat more
maxcap = 68750 # capacitance reading for full supply level
mincap = 11484 # capacitance reading when tank is empty
NumberOfTanks=2
if NumberOfTanks>1:
maxlitres *= NumberOfTanks
minlitres *= NumberOfTanks
slope=(maxlitres-minlitres)/float(maxcap-mincap)
base=minlitres-slope*mincap
def compensate(level,temp,ntanks=NumberOfTanks):
''' returns a temperature compensated tank level (NOT litres)'''
level=level+213*(temp-28.2)*(level/73035.0)
return level
def convert(level):
'''returns the (uncompensated) volume corresponding to the
capacitance meter output value @level.
'''
litres=base+slope*float(level)
return litres
def read_tank_state():
f=open('/home/ajh/logs/central/tankState','r')
l=f.readline()
if len(l) <= 1: return
l = string.split(l)
if len(l) < 3: return
(level, temp) = map(int, l[1:3]);
return (level, temp)
def calibrated():
tankdepth,tanktemp=read_tank_state()
bigtankheight=1450
tankdepth = (float(tankdepth-mincap)/float(maxcap-mincap))*bigtankheight
volume = compensate(tankdepth,tanktemp)
return (volume,tanktemp)
def readraw(dev):
device=os.open(dev,os.O_RDWR)
res=''
while not res:
res=os.read(device,14)
time.sleep(5)
os.close(device)
return res
def main():
(level,temp)=read_tank_state()
(volume, temp) = calibrated()
format = "capacitance reading = %f, "
format += "tank temperature = %f"
print format % (level, temp*0.1)
uncompensated=convert(level)
print "uncompensated volume = %5.1f" % (uncompensated)
if __name__ == '__main__':
(vals,path)=getopt.getopt(sys.argv[1:],'',[])
usb=usbFind.USBclass()
tankDevice=usb.device2port('tank')
lastMin=60
while True:
rawtank=readraw(tankDevice)
res=re.match('^ *(\d+) +(\d+) +(\d+).*$',rawtank)
if res:
level=int(res.group(1))
volts=int(res.group(3))
now=datetime.datetime.now()
thisMin=now.minute
now=now.strftime("%Y%m%d:%H%M%S")
if thisMin!=lastMin:
logfile=open(LOGFILENAME,'a')
logfile.write("%s %d %d\n" % (now,level,volts))
logfile.close()
#print "%s %d %d" % (now,level,volts)
lastMin=thisMin
pass # end if
pass # end while
The temperature compensation values were worked out from a pair
of observations:
- On 16 Jan 2014 at 0800, the tank level raw indicator was
73035, while the (outside) temperature was recorded as 28.2
- At 1200 (midday), the tank level was 73248, and the
temperature was 41.5 (yes, it was hot!)
This gives a line of slope 16.015038 and origin abscissa of
72583.38 for this tank level, assuming that the raw level
indicator (corresponding to the frequency oscillator in the
water level converter circuit) is linearly related to
temperature.
We further assume that these two values themselves are linearly
related as the tank level falls, and they themselves should be
linearly adjusted by tank level, to get a generalized
compensation calculation.
Further work remains to be done on this aspect of tank logging,
in particular, how the temperature and raw level data are
retrieved and correlated.
7. The Solar System
This is almost verbatim from the central version,
although some modifications have been made to correct what
appeared to be the presence of obsolete code in
read_register.
This code is responsible for building the solar.log
file. It is to be run every minute on garedelyon
(c.f. EveryMinute.sh), and makes calls upon the
pl60 module (a C program) to read the solar data from the
solar controller, register by register (Need a link to the solar
controller manual here). The log entry is then printed to
standard output for logging, and to the file solarState
which records the most recent value.
These are the registers of the pl60:
17 |
Battery Vmax |
20 |
Charge AH |
24 |
Load AH |
32 |
Input Current |
34 |
Battery Voltage |
Note that the values returned by these registers (may) need
recalibration. Others not mentioned are not used.
7.1 logsolar.py
"logsolar.py" 7.1 =#!/usr/bin/python
<edit warning 2.1>
import cgi,string,os
import time,sys
from HouseDefinitions import CENTRAL
LOGS='/home/ajh/logs/%s/' % CENTRAL
SOLARSTATEFILE=LOGS+'solarState'
SOLARLOGFILE =LOGS+'solar.log'
(year, month, day, hour, minute, second,
weekday, yday, DST) = time.localtime(time.time())
import cgi
def read_register(i):
f = os.popen("/home/ajh/bin/pl60 -r %d" % i, "r")
line=f.readline()
#print "register %d => %s" % (i,line)
out = string.split(string.strip(line))[-1]
#print out
f.close()
return out
def int_register(i):
return int(read_register(i))
in_Ah = int_register(20) # Charge AH
out_Ah = int_register(24) # Load AH
solaramps = int_register(32)*0.4 # Input Current
solarbatteryvolts = int_register(34)*0.1+15 # Battery Voltage
solarpower = solaramps*solarbatteryvolts
percentsolar = solaramps * 100.0 / 50.0
in_whr = int(in_Ah*27.6)
in_MJ = (in_Ah*27.6*3.6/1000)
dt = time.strftime("%Y%m%d:%H%M")
stateline="%s %d %d %d %d %d %d" % \
(dt,in_Ah,out_Ah,solaramps,solarbatteryvolts,solarpower,in_whr)
# this gets redirected at shell level
print stateline
f=open(SOLARSTATEFILE,'w')
f.write(stateline+'\n')
f.close()
There is no need to explicitly start this program running, as
it is called once every minute by the EveryMinute.sh
cron script.
7.2 solar.py
Provide definitions and access functions for the solar
controller.
"solar.py" 7.2 =#!/usr/bin/python
<edit warning 2.1>
import cgi
import os
import string
import time
def read_register(i):
f = os.popen("pl60 -b %d" % i, "r")
line=f.readline()
out = int(line)
f.close()
return out
def int_register(i):
return int(read_register(i))
def float_register(i):
val=float(read_register(i))
if i==32: val=0.4*val
return val
def main():
in_Ah = int_register(20)
out_Ah = int_register(24)
solaramps = int_register(32)*0.4
solarbatteryvolts = int_register(34)*0.1+15
solarpower = solaramps*solarbatteryvolts
percentsolar = solaramps * 100.0 / 43.0
t = time.time()
dt = time.strftime("%Y%m%d")
tm = time.strftime("%H:%M")
WHcost = 0.00013
numPanels = 20
data_Panel = 0.0
print "in_Ah=%4.1f, out_Ah=%4.1f, solaramps=%4.1f" %\
(in_Ah, out_Ah, solaramps)
if __name__ == "__main__":
main()
Note that this code is intended to be imported as a module to
python programs that need access to the solar controller. It
may be called as a stand-alone program, when it simply prints
key data and exits.
8. The Chook Door
The chook house (described separately in the Chickens page) has a door
that is automatically controlled, and opens and shuts in
accordance with sunrise and sunset times throughout the year.
important note! The chook door proving system has
recently been changed from a micropython board to a Beaglebone
Black. Consequently some of the following is out-of-date. It
will be updated as soon as possible. In the interim, a
breadboarded system is running the proving subsystem, and it is
not fully integrated into the rest of the system.
There are three programs in this suite:
-
ChookDoor.py
This code provides a module for managing the chook door operations.
-
checkChooks.py
This code updates the houseState file with the
current state of the chook door, by calling on the
readChook.py code.
-
ChookProve.py This code reads the single bit
value that proves that the chook door is shut, and writes
the current state as a text line containing either
'closed' or 'open' to the file
/home/ajh/Computers/House/status. It must be run on
the Beaglebone system (auteuil) as root, in order to
be able to access the (privileged) GPIO pins.
"ChookDoor.py" 8.1 =
<edit warning 2.1>
import currentState
import datetime
import ephem
import getopt
import re
import sys
from HouseDefinitions import *
compute=0
version='1.0.0'
chookFileName='/home/ajh/Computers/House/suntimes.txt'
now=datetime.datetime.now()
def parse(pat,line):
res=re.match(pat,line)
if res:
return res.group(1)
else:
return ''
class ChookDoor():
def __init__(self):
self.opendelay=60
self.shutdelay=20
self.lastrun=''
self.current='open'
pass
<ChookDoor: load 8.2>
<ChookDoor: compute 8.3>
<ChookDoor: save 8.4>
def main():
chooks=ChookDoor()
chooks.load()
if compute:
chooks.compute()
chooks.save()
print "chooks lastrun = %s" % (chooks.lastrun)
print "now = %s" % (now.strftime("%Y%m%d:%H%M"))
print "opendelay = %s" % (chooks.opendelay)
print "shutdelay = %s" % (chooks.shutdelay)
print "today sunrise = %s" % (chooks.sunrise)
print "today sunset = %s" % (chooks.sunset)
print "dooropen = %s" % (chooks.dooropen)
print "doorshut = %s" % (chooks.doorshut)
print "chook door is %s" % (chooks.current)
if __name__ == '__main__':
(vals,path)=getopt.getopt(sys.argv[1:],'cVn=',
['compute','now=','version'])
for (opt,val) in vals:
if opt=='-c' or opt=='--compute':
compute=1
if opt=='-n' or opt=='--now':
now=datetime.datetime.strptime(val,"%Y%m%d:%H%M")
if opt=='-V' or opt=='--version':
print version
sys.exit(0)
main()
To use this module, import ChookDoor into your
code, and create an instance of the class ChookDoor.
Data computed by this class is stored in a persistent form
in the file
chookFileName='/home/ajh/Computers/House/suntimes.txt'.
The following methods and entities are available:
- load()
-
Initialize the instance with data from the persistent
record.
- compute()
- Recompute the data in the persistent record (but don't
store it).
- save()
- Save the persistent data (if necessary).
- sunrise
- Today's sunrise time.
- sunset
- Today's sunset time.
- dooropen
- Today's door opening time.
- doorshut
- Today's door shutting time.
- opendelay
- minutes after sunrise that the door is to be opened
(default is 60 minutes).
- shutdelay
- minutes after sunset that the door is to be shut
(default is 20 minutes).
- current
- The current state of the door, either
'open' or 'shut'. (Setting this
variable has no effect on the door: it is set only as a
side effect of the compute() method.)
If the module is invoked as a main program, it performs a load
and print operation, unless the -c option is supplied,
when it does a recompute of the ephemeral data (and opens/shuts
the door if required).
8.1 ChookDoor: load
<ChookDoor: load 8.2> =def load(self):
try:
suntimefile=open(chookFileName,'r')
innow=suntimefile.readline()
self.lastrun=parse('now += (.*)$',innow)
self.now=self.lastrun
inopdel=suntimefile.readline()
self.opendelay=parse('opendelay += (.*)$',inopdel)
inshdel=suntimefile.readline()
self.shutdelay=parse('shutdelay += (.*)$',inshdel)
inrise=suntimefile.readline()
self.sunrise=parse('sunrise += (.*)$',inrise)
inset=suntimefile.readline()
self.sunset=parse('sunset += (.*)$',inset)
inopen=suntimefile.readline()
self.dooropen=parse('dooropen += (.*)$',inopen)
inshut=suntimefile.readline()
self.doorshut=parse('doorshut += (.*)$',inshut)
incurrent=suntimefile.readline()
self.current=parse('door is +(.*)$',incurrent)
suntimefile.close()
except IOError:
pass
pass
Read the persistent data from the external file. This is to
allow queries of the data without necessarily recomputing the
ephemeral data.
8.2 ChookDoor: compute
<ChookDoor: compute 8.3> =def compute(self):
<ChookDoor: compute ephemeral values 8.5>
self.now=now
dooropen=sunrise+datetime.timedelta(0,0,0,0,int(self.opendelay))
doorshut=sunset+datetime.timedelta(0,0,0,0,int(self.shutdelay))
self.dooropen=dooropen.strftime("%Y%m%d:%H%M")
self.doorshut=doorshut.strftime("%Y%m%d:%H%M")
self.sunrise=sunrise.strftime("%Y%m%d:%H%M")
self.sunset=sunset.strftime("%Y%m%d:%H%M")
lastState=self.current
house=currentState.HouseState()
house.load()
#print "now=%s, dooropen=%s, doorshut=%s" % (now,dooropen,doorshut)
if now<dooropen or now>=doorshut:
self.current='shut'
if lastState=='open':
# shut the door
Central.start(1,20)
house.store('chookDoor','shut')
house.save()
elif now>=dooropen and now<doorshut:
self.current='open'
if lastState=='shut':
# open the door
Central.start(0,20)
house.store('chookDoor','open')
house.save()
else:
print "bad compare: now=%s, dooropen=%s, doorshut=%s" % (now,dooropen,doorshut)
Check if the current time requires a change in the door
status. If so, open or close the door as required.
8.3 ChookDoor: save
<ChookDoor: save 8.4> =def save(self):
suntimefile=open(chookFileName,'w')
suntimefile.write("now = %s\n" % (self.now.strftime("%Y%m%d:%H%M"))) # now
suntimefile.write("opendelay = %s\n" % (self.opendelay)) # opendelay
suntimefile.write("shutdelay = %s\n" % (self.shutdelay)) # shutdelay
suntimefile.write("sunrise = %s\n" % (self.sunrise)) # sunrise
suntimefile.write("sunset = %s\n" % (self.sunset)) # sunset
suntimefile.write("dooropen = %s\n" % (self.dooropen)) # dooropen
suntimefile.write("doorshut = %s\n" % (self.doorshut)) # doorshut
suntimefile.write("door is %s\n" % (self.current)) # current state
suntimefile.close()
pass
8.4 ChookDoor: compute ephemeral values
<ChookDoor: compute ephemeral values 8.5> =now=datetime.datetime.now()
thisday=now.day
sun=ephem.Sun()
melb=ephem.city('Melbourne')
prevsunrise=melb.previous_rising(sun)
prevsunset=melb.previous_setting(sun)
nextsunrise=melb.next_rising(sun)
nextsunset=melb.next_setting(sun)
# local time zone adjustment
prevsunrise=prevsunrise.datetime()+datetime.timedelta(0,0,0,0,0,10)
prevsunset =prevsunset.datetime()+datetime.timedelta(0,0,0,0,0,10)
nextsunrise=nextsunrise.datetime()+datetime.timedelta(0,0,0,0,0,10)
nextsunset =nextsunset.datetime()+datetime.timedelta(0,0,0,0,0,10)
#now compute *today*'s rise/setting
if thisday==prevsunrise.day:
# use previous
sunrise=prevsunrise
else:
# use next
sunrise=nextsunrise
if thisday==prevsunset.day:
# use previous
sunset=prevsunset
else:
# use next
sunset=nextsunset
Use the Python Ephemeral package to compute sunrise and
sunset times for Melbourne. Note that the interfaces call for
the previous and next times, not today's times,
and hence there is a bit of computation needed to work out
today's times, regardless of whether the actual sunrise/sunset
time is before or after the current time. The times are all
computed in UTC, and hence there is a necessary adjustment for
local time. Note that daylight saving time is not
taken into account!
8.5 checkChooks: update the current chook door state
"checkChooks.py" 8.6 =#!/usr/bin/python
<edit warning 2.1>
import currentState
import datetime
import xmlrpclib
LOGFILE='/home/ajh/logs/central/chookDoor.log'
auteuil=xmlrpclib.ServerProxy('http://10.0.0.117:8001')
door=auteuil.getDoorState()
#print door
house=currentState.HouseState()
house.load()
house.store('chookDoorProve',door)
house.save()
timestamp=datetime.datetime.now()
timestamp=timestamp.strftime("%Y%m%d:%H%M")
f=open(LOGFILE,'a')
f.write("%s: %s\n" % (timestamp,door))
f.close()
This code runs every minute, and inspects the current state
of the chook door as given by the door proving subsystem. It
uses this to update the general house state file,
houseState, as otherwise maintained by <currentState.py 9.1>
8.6 Chook Door Proving Subsystem
Another attempt has been made to employ a Beaglebone system,
this time just to run the hard subsystems. The first piece of
hardware to be incorporated is the Chook Door Proving Circuit.
This is a completely separate circuit (shared a common ground,
though) that uses a separate microswitch to detect when the
door is properly closed. The microswitch closes on shutting,
and the Beaglebone GPIO pins are used a) to read the
microswitch, and b) to light some indicator LEDs to show the
door state. Red is used to indicate open, Green for
closed.
Important Note: It would make sense to combine these
two scripts, and eliminate the extra latency occasioned by
having to write the status to a file. However, the need for
the first script to run as root might complicate matters
somewhat.
"ChookProve.py" 8.7 =
<edit warning 2.1>
import Adafruit_BBIO.GPIO as GPIO
import time
greenpin="P8_11"
timegreen=0.1
redpin="P8_12"
timered=1
inpin="P8_10"
GPIO.setup(redpin,GPIO.OUT)
GPIO.setup(greenpin,GPIO.OUT)
GPIO.setup(inpin,GPIO.IN)
last="unknown"
while True:
if GPIO.input(inpin):
new="open"
GPIO.output(greenpin,GPIO.LOW)
GPIO.output(redpin,GPIO.HIGH)
else:
new="closed"
GPIO.output(redpin,GPIO.LOW)
GPIO.output(greenpin,GPIO.HIGH)
time.sleep(timegreen)
if new!=last:
print(new)
f=open('status','w')
f.write("%s\n" % new)
f.close()
last=new
This is a simple program that sets up the GPIO pins, then
reads the input to check the door stete, lights the
appropriate led, and write a simple status into a file.
8.7 Chook Door Proving Server
"ChookDoorServer.py" 8.8 =#!/usr/bin/python
<edit warning 2.1>
import datetime
import re
import subprocess
from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
# Restrict to a particular path.
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/RPC2',)
# Create server
server = SimpleXMLRPCServer(("10.0.0.117", 8001),
requestHandler=RequestHandler)
server.register_introspection_functions()
# Define and Register the getHeating function
def getDoorState():
p=open('/home/ajh/Computers/House/status','r')
openclosed=p.readline().strip()
p.close()
return openclosed
server.register_function(getDoorState, 'getDoorState')
# Run the server's main loop
print "Chook Door Server restarts"
server.serve_forever()
This is an RPC-based server, based upon the <RelayServer.py 10.2> system, that provides a
single procedure getDoorState which returns the
current state of the chook door, as measured by the proving
circuit.
9. The House Computer
9.1 The Current State Interface
"currentState.py" 9.1 =#!/usr/bin/python
Debug=False
STATEFILE='/home/ajh/logs/central/houseState'
class HouseState():
def __init__(self):
state={}
def load(self,fname=STATEFILE):
f=open(fname,'r')
statedata=f.read()
if Debug: print statedata
self.state=eval(statedata)
f.close()
def get(self,name):
if self.state.has_key(name):
return self.state[name]
else:
print "invalid name %s" % (name)
return None
def store(self,name,value):
self.state[name]=value
def save(self,fname=STATEFILE):
fn=fname
if Debug: fn+='2'
f=open(fn,'w')
f.write('{\n')
for k in self.state.keys():
if Debug: print "saving %s:%s" % (k,self.state[k])
value=self.state[k]
if isinstance(value,str):
f.write(" '%s':'%s',\n" % (k,value))
else:
f.write(" '%s':%s,\n" % (k,value))
f.write('}\n')
f.close()
def main():
house=HouseState()
house.load()
if Debug: print house.get('test')
house.save()
if __name__=='__main__':
main()
The currentState routine provides an easy access
mechanism to get the current state of various house values, as
computed by the various routines. This is in the form of a
module that is to be imported by any routine changing the
value of a key variable, and provides persistent storage of
that variable, until the next time it may be updated.
9.2 The HouseData Server (obsolete)
Defines an RPC server to provide details of the current
house state (e.g., the contents of the heating file, the log
files, etc.), and to update data as required..
Most of this code was stolen from the Python Library
Reference document, and revised from the SimpleHeatExchanger.
This code has been decommissioned, as the server (garedelyon)
is defunct. All of these functions are now available through
the main house server (lilydale).
"HouseData.py" 9.2 =#!/usr/bin/python
<edit warning 2.1>
import datetime
import re
import subprocess
import tank
import solar
from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
# Restrict to a particular path.
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/RPC2',)
# Create server
server = SimpleXMLRPCServer(("10.0.0.101", 8001),
requestHandler=RequestHandler)
server.register_introspection_functions()
# define some crucial patterns
# first, for parsing the temperature (weather station) log file:
temppat='(\d\d\d\d\d\d\d\d:\d\d\d\d\d\d)' # date and time
temppat+=' +([0-9.]+)' # inside temp
temppat+=' +([0-9.]+)' # outside temp
temppat+=' +([0-9.]+)' # inside humidity
temppat+=' +([0-9.]+)' # outside humidity
temppat+=' +([0-9.]+)' # outside dew point
temppat+=' +([0-9.]+)' # heating thermostat
temppat+=' +(.+)$' # heating on/off
temppat=re.compile(temppat)
maxminpat='(\d\d\d\d\d\d\d\d)' # date only
maxminpat+=' +([0-9.]+)' # maximum temp
maxminpat+=' +([0-9:]+)' # maximum temp time
maxminpat+=' +([0-9.]+)' # minimum temp
maxminpat+=' +([0-9:]+)' # minimum temp time
maxminpat=re.compile(maxminpat)
# Define and Register the getHeating function
def getHeating():
p=open('/logdisk/logs/heating','r')
temp=float(p.readline())
onoff=p.readline().strip()
p.close()
return (temp,onoff)
server.register_function(getHeating, 'getHeating')
# Define and Register the setHeating function
def set(temp,onoff):
p=open('/logdisk/logs/heating','w')
p.write("%5.2f\n%s\n" % (temp,onoff))
p.close()
return 'OK'
server.register_function(set, 'setHeating')
# Define and Register the getSolar function
# moved to RelayServer, 20150509:113400
<HouseData define getTemps 9.3>
server.register_function(getTemps, 'getTemps')
<HouseData define maxminTemp 9.4>
server.register_function(maxminTemp, 'maxminTemp')
# Run the server's main loop
print "HouseData restarts"
server.serve_forever()
Note that the water log should be moved from the root
directory to the /logdisk/logs directory to be
consistent. Also, we should find a way to manage the bound on
the size of the log files (getting the maximum is O(n), for
example, whereas it should be O(hours)).
9.2.1 HouseData define getTemps
<HouseData define getTemps 9.3> =# define the get water level function
def getTemps():
logfile='/logdisk/logs/temp.log'
cmd=['/usr/bin/tail','-1',logfile]
pipe=subprocess.Popen(cmd,stdout=subprocess.PIPE)
p=pipe.stdout
l=p.readline()
p.close()
print l
res=temppat.match(l)
if res:
intemp=float(res.group(2))
outtemp=float(res.group(3))
else:
intemp=0.0
outtemp=20.0
return (intemp,outtemp)
9.2.2 HouseData define maxminTemp
<HouseData define maxminTemp 9.4> =# Define the max and min temperature function
# returns a table of maxima and minima, computed previously
def maxminTemp():
maxmintable={}
logfile='/logdisk/logs/maxmins.log'
f=open(logfile,'r')
for l in f.readlines():
res=maxminpat.match(l)
if res:
d=res.group(1)
max=res.group(2)
maxat=res.group(3)
min=res.group(4)
minat=res.group(5)
maxmintable[d]=(max,maxat,min,minat)
f.close()
return maxmintable
9.3 The startHouseData.sh script
"startHouseData.sh" 9.5 =#!/bin/bash
<edit warning 2.1>
LOGDIR=/logdisk/logs
HOUSEPROC=$LOGDIR/houseProcess
HOUSEDIR=/home/ajh/Computers/House
kill -9 `cat $HOUSEPROC`
/usr/bin/python $HOUSEDIR/HouseData.py >> $LOGDIR/housedata.log 2>&1 &
ps aux | grep "HouseData.py" | grep -v grep | awk '{print $2}' >$HOUSEPROC
10. The House Data Server and Relay Control System
Currently, the laptop lilydale is used as both relay
controller (through an Arduino circuit) and as general data
logger and server for the various house functions. (An
exception is the Chook Proving system, see section Chook Door Proving system.)
In this table, the relays are numbered left to right on the
computer house panel. Bit 0 is the most significant (or
leftmost) bit.
Relay |
Name |
Function |
Wire Colour |
0 |
|
|
|
1 |
|
|
|
2 |
|
|
|
3 |
TopUp |
Top Up tanks from Mains |
|
4 |
BottomVegBed |
Bottom Vegetable Bed |
Brown |
5 |
MiddleVegBed |
Middle Vegetable Bed |
Green |
6 |
TopVegBed |
Top Vegetable Bed |
Red |
7 |
CarportVegBed |
Carport Vegetable Bed |
White |
8 |
RainForest |
Rain Forest Sprayers |
White |
9 |
Woo2Plas |
WooTank to PlasTank |
10 |
FloodNDrain |
Flood And Drain |
11 |
Heating |
Heating |
Possibilities for the new relays:
10.1 The Relay Controller Code
This is a simple standalone program used by cron jobs to
turn on relays at various times (mainly watering) for fixed
periods of time. It calls the RelayServer via RPC calls to
actually drive the relays, and really serves only as a CLI
parameter handler. The relay name, and the time it is to turn
on are supplied by two CLI parameters. If other than two
parameters (besides the program name) are supplied, a default
is used.
"RelayControl.py" 10.1 =import time
import os
import sys
import xmlrpclib
from HouseDefinitions import *
def main(device,timeRunning):
# get relay bit for device
try:
bitNo=RelayTable[device]
except KeyError:
print "bad relay function key %s" % (relayFunction)
sys.exit(1)
# start the relay for timeRunning seconds
(state,ok)=Central.start(bitNo,timeRunning)
print "relays set to %s" % (state)
pass
if __name__ == '__main__':
if len(sys.argv)==3:
# get relay name
device=sys.argv[1]
# get relay time
timer=int(sys.argv[2])
else:
# default if insufficient parameters
device='FloodNDrain'
timer=20
main(device,timer)
10.2 Relay Server
The relay server runs all the time, offering a RPC interface
to the relay driver. The relay state is represented as a
n-element list, where each element represents the relay state
as an integer 1 (relay on) or 0 (relay off). It can be
controlled either by passing a full state list, or by turning
individual bits on and off. The latter is to be preferred, to
avoid parallel interaction conflicts.
"RelayServer.py" 10.2 =#!/usr/bin/python
import datetime
import os
import re
import solar
import sys
import subprocess
import threading
import time
import usbFind
from SimpleXMLRPCServer import SimpleXMLRPCServer
from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler
from HouseDefinitions import NumberOfRelays,RelayNames
# define the relay controller device
usb=usbFind.USBclass()
relayDevice=usb.device2port('arduino')
relayDevice="/dev/ttyUSB%1d" % (relayDevice)
# Restrict to a particular path.
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/RPC2',)
# Create server
# 10.0.0.112 = lilydale
server = SimpleXMLRPCServer(("10.0.0.112", 8001),
requestHandler=RequestHandler)
server.register_introspection_functions()
# define the relay state
currentState=[0 for i in range(NumberOfRelays)]
currentTime =[0 for i in range(NumberOfRelays)] # time on in seconds
nonZeroTimes=[] # those relays on for some time
redundantChanges=[0 for i in range(NumberOfRelays)] # count idempotent ops
# define the relay control channel
relayChannel=open(relayDevice,'w')
# open the logfile
logname="/home/ajh/logs/central/RelayServer.log"
logs=open(logname,'a')
# define the convert state to string function
def strState(state):
str=''
for i in range(NumberOfRelays):
if currentState[i]==0:
str += '0'
else:
str += '1'
return str
# Define and Register the getTank function
<relayserver: define getTank 10.3>
server.register_function(getTank, 'getTank')
# Define and Register the getTimer function
<relayserver: getTimer 10.4>
# Define and Register the getState function
<relayserver: getState 10.5>
# Define and Register the setState function
<relayserver: setState 10.6>
# Define and Register the setBit function
<relayserver: setBit 10.7>
# Define and Register the setBitOn function
<relayserver: setBitOn 10.8>
# Define and Register the setBitOff function
<relayserver: setBitOff 10.9>
# Define and Register the start function
<relayserver: start 10.10>
# define the count down timers process
<relayserver: countDown 10.11>
# Define and Register the getSolar function
def getSolar(regNo):
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
reg=solar.float_register(regNo)
#logs.write("%s: getSolar(%d)=>%4.1f\n" % (now,regNo,reg))
#logs.flush(); os.fsync(logs.fileno())
return reg
server.register_function(getSolar, 'getSolar')
# Run the server's main loop
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
logs.write("%s: RelayServer restarts\n" % (now))
logs.flush(); os.fsync(logs.fileno())
counters=countDown()
counters.start()
server.serve_forever()
counters.join()
logs.close()
10.2.1 HouseData define getTank
Here we make a stab at applying a temperature compensation
to the water level. It assumes that the compensation is
linear in both temperature and level, a somewhat bold
assumption.
<relayserver: define getTank 10.3> =# define the get water level function
def getTank():
# dynamically import tank, so that we get latest data settings
import tank
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
statefile='/home/ajh/logs/central/tankState'
p=open(statefile)
l=p.readline()
p.close()
res=re.match('^(\d\d\d\d\d\d\d\d:\d\d\d\d\d\d) +(\d+) ',l)
if res:
level=int(res.group(2))
else:
level=-1
# now temperature compensate the level, and calibrate
litres=tank.convert(level)
#logs.write("%s getTank()=>%s (from level=%s)\n" % (now,litres,level))
#logs.flush(); os.fsync(logs.fileno())
return litres
10.2.2 relayserver: getTimer
<relayserver: getTimer 10.4> =def getTimer(bitNo):
remTime=currentTime[bitNo]
return remTime
server.register_function(getTimer, 'getTimer')
10.2.3 relayserver: getState
<relayserver: getState 10.5> =def getState():
return currentState
server.register_function(getState, 'getState')
10.2.4 relayserver: setState
<relayserver: setState 10.6> =def setState(newState):
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
if len(newState) < NumberOfRelays:
errmsg="%s incomplete state %s in call to setState"
print errmsg % (now,newState)
return (currentState, errmsg % (newState))
for i in range(NumberOfRelays):
currentState[i]=newState[i]
s=strState(currentState)
relayChannel.write(s+'\n')
logs.write("%s setState(%s)\n" % (now,s))
logs.flush(); os.fsync(logs.fileno())
return (currentState,"OK")
server.register_function(setState, 'setState')
10.2.5 relayserver: setBit
<relayserver: setBit 10.7> =def setBit(bitNo,newValue):
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
if bitNo>=NumberOfRelays:
errmsg="%s bad bit number %d in call to setBit"
print errmsg % (now,bitNo)
return (currentState, errmsg) % (now,bitNo)
oldState=currentState[bitNo]
currentState[bitNo]=newValue
s=strState(currentState)
relayChannel.write(s+'\n')
if oldState!=newValue:
stateStr=['Off','On'][newValue]
c=redundantChanges[bitNo]
r="previous change repeated %d times" % (c)
logs.write("%s setBit%s(%d) newstate=%s, %s (%s)\n" % \
(now,stateStr,bitNo,s,RelayNames[bitNo],r))
logs.flush(); os.fsync(logs.fileno())
redundantChanges[bitNo]=0
else:
redundantChanges[bitNo]+=1
return (currentState,"OK")
server.register_function(setBit, 'setBit')
This new routine is intended to coalesce the operations of
setBitOn and setBitOff, by passing in an extra
parameter newValue.
setBit sets the relay control word to its current
state, and with bit number bitNo set to
newValue. This new word is written to the relay
driver, via the relayChannel routine and the Arduino
controller.
An additional piece of logic checks to see if this is
actually a change of state, and if it is not, avoids logging
the superfluous set operation, but rather increments a
counter which is output when the bit is actually changed.
Note that this only affects logging - the controller is
still updated with the new (unchanged) state.
10.2.6 relayserver: setBitOn
<relayserver: setBitOn 10.8> =def setBitOn(bitNo):
return setBit(bitNo,1)
server.register_function(setBitOn, 'setBitOn')
setBitOn sets the relay control word to its current
state, and with bit number bitNo set to a 1. This
new word is written to the relay driver, via the
relayChannel routine and the Arduino controller.
10.2.7 relayserver: setBitOff
<relayserver: setBitOff 10.9> =def setBitOff(bitNo):
return setBit(bitNo,0)
server.register_function(setBitOff, 'setBitOff')
setBitOff sets the relay control word to its current
state, and with bit number bitNo set to a 0. This
new word is written to the relay driver, via the
relayChannel routine and the Arduino controller.
10.2.8 relayserver: start
<relayserver: start 10.10> =def start(bitNo,timeon):
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
print "%s: Central.start(%d,%4.1f)\n" % (now,bitNo,timeon)
if bitNo>=NumberOfRelays:
errmsg="%s bad bit number %d in call to start"
print errmsg % (now,bitNo)
return (currentState, errmsg) % (now,bitNo)
currentState[bitNo]=1
s=strState(currentState)
logs.write("%s: startTimer(%d,%4.1f), newstate=%s (%s)\n" % (now,bitNo,timeon,s,RelayNames[bitNo]))
logs.flush(); os.fsync(logs.fileno())
# design decision: timeon is relative, not absolute
currentTime[bitNo]+=timeon
if bitNo not in nonZeroTimes:
nonZeroTimes.append(bitNo)
s=strState(currentState)
relayChannel.write(s+'\n')
# turning the bit off is taken care of by the countDown process
return (currentState,"OK")
server.register_function(start, 'start')
10.2.9 relayserver: countDown
<relayserver: countDown 10.11> =class countDown(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
while True:
if nonZeroTimes:
for bitNo in nonZeroTimes:
currentTime[bitNo]-=1
if currentTime[bitNo]==0:
# turn this bit off and log the fact
currentState[bitNo]=0
s=strState(currentState)
relayChannel.write(s+'\n')
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
print "%s: stopTimer(%d), newstate=%s (%s)" % (now,bitNo,s,RelayNames[bitNo])
logs.write("%s: stopTimer(%d), newstate=%s (%s)\n" % (now,bitNo,s,RelayNames[bitNo]))
logs.flush(); os.fsync(logs.fileno())
# remove from nonZeroTimes
nonZeroTimes.remove(bitNo)
time.sleep(1) # sleep until next cycle
The purpose of auxilary process countDown is to
maintain the count of how long each relay is to be held
closed, if it was started with a start call. Note
that it is possible to also set relays on and off without a
time (using the setBitOn and setBitOff RPC
calls), in which case the time remaining is shown as 0
(zero). Such calls will not be automatically turned off,
since they do not set the list of nonZeroTimes.
Once a second, the process awakes, and decrements any
non-zero counts. These non-zero counts are indicated in the
list nonZeroTimes as a partial optimization to avoid
testing all relay counts, and to avoid ambiguity about which
relays are independently turned on.
10.3 Start the Relay Controller
This short script encapsulates all that is necessary to
(re)start the relay server. It records the process ID in the
file relayProcess so that when it is restarted, any
previous invocation is removed properly.
"startRelayServer.sh" 10.12 =LOGDIR='/home/ajh/logs/central'
HOUSE='/home/ajh/Computers/House'
BIN=${HOME}/bin
if [ -f ${LOGDIR}/relayProcess ] ; then
for p in `cat ${LOGDIR}/relayProcess` ; do
kill -9 $p
done
fi
rm ${LOGDIR}/relayProcess
#/usr/bin/python ${HOUSE}/RelayServer.py `${BIN}/getDevice arduino` >${LOGDIR}/relay.log 2>&1 &
# use non-logging version for now
/usr/bin/python ${HOUSE}/RelayServer.py `${BIN}/getDevice arduino` >/dev/null 2>&1 &
ps aux | grep "RelayServer.py" | grep -v grep | awk '{print $2}' >${LOGDIR}/relayProcess
10.4 The Old Relay Setup
These are the connections for the old I2C setup that used to
run off central, and now decomissioned. They are
recorded here to aid in back-tracing legacy connections.
0/0 |
solarwash |
1/0 |
heating |
0/1 |
rainforest |
1/1 |
nc |
0/2 |
seedling |
1/2 |
nc |
0/3 |
alpine |
1/3 |
rainforestdrip |
0/4 |
floodndrain |
1/4 |
nc |
0/5 |
woo2plas |
1/5 |
bypass |
0/6 |
flush |
1/6 |
northseedling |
0/7 |
creek |
1/7 |
topup |
10.5 The Arduino Interface
10.5.1 The Arduino Start Script
"startArduino.sh" 10.13 =source setupMake.sh
make upload
Run this script in the
/home/ajh/Computers/House/RelayDriver8way directory
on bastille. The setup file sets a few key environment
variables.
"setupMake.sh" 10.14 =export ARDUINODIR=/usr/share/arduino
export BOARD=atmega328
export SERIALDEV=`/home/ajh/bin/getDevice arduino`
The make upload command should upload the driver
program, and make the arduino ready to accept commands from
the command line and the RelayControl.py script. To
test that this is working, try:
echo 001000000000 >/dev/ttyUSB0
(ensure that the device named is consistent with the device
used in the upload script. Due to the dynamic nature of the
usb "plug and play" model, this can change easily,
especially when the usb connections have been disturbed.)
11. Hardware Issues
11.1 USB Issues
One of the big headaches in getting this system operational has
been the mapping of USB devices to the correct hardware ports.
This is clearly a long-standing issue, as many experimenters have
posted questions about this on the web.
One of the key software tools to aid in this process of finding
the correct mapping is the lsusb command under Linux.
Here is the output from it on bastille at 20150426:110547
timestamp (reordered to place items in bus/device order):
ajh@bastille /home/ajh/Computers/House 517 $ date;lsusb
20150426:110547
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
Bus 001 Device 002: ID 2109:2812
Bus 001 Device 003: ID 2109:2812
Bus 001 Device 004: ID 0557:2008 ATEN International Co., Ltd UC-232A Serial Port [pl2303]
Bus 001 Device 005: ID 0557:2008 ATEN International Co., Ltd UC-232A Serial Port [pl2303]
Bus 001 Device 006: ID 067b:2303 Prolific Technology, Inc. PL2303 Serial Port
Bus 001 Device 007: ID 0403:6001 Future Technology Devices International, Ltd FT232 USB-Serial (UART) IC
Bus 001 Device 008: ID 0bc2:2320 Seagate RSS LLC
Bus 002 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
This is my translation of what these actually are:
Bus |
Port |
Device |
Hardware |
1 |
1 |
|
BeagleBone server USB hub |
1 |
2 |
(not in use) |
|
1 |
3 |
(not in use) |
|
1 |
4 |
/dev/ttyUSB0 |
solar |
1 |
5 |
/dev/ttyUSB1 |
tank |
1 |
6 |
(not in use) |
weather |
1 |
7 |
/dev/ttyUSB2 |
Arduino |
1 |
8 |
(not in use) |
|
2 |
1 |
|
BeagleBone client hub |
Note that at the moment several devices (some of which are
not in use) on bus 1 cannot be resolved, as they use the same
brand of USB/RS232 cable connectors, and there appears to be
no serial number information. Note that the IDs are the same
as well.
Micheal Ludvig's very helpful page at
http://hintshop.ludvig.co.nz/show/persistent-names-usb-serial-devices/
shows how to setup persistent names for these USB-serial
devices, but unfortunately it relies upon having unique
IDs/SerialNumbers for each device on the bus. You will note that
the two ATEN devices have the same Vendor/Product pair, and NO
serial numbers, so that they cannot be distinguished at this
level. I have posted a comment about this, and will revisit it
when/if I receive a reply.
Here are the udev rules I have set up in
/etc/udev/rules.d/99-usb-serial.rules to handle my
devices:
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", SYMLINK+="arduino"
SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2302", SYMLINK+="tank"
(20150606:152721) Note: the Seagate disk has been recording
disk errors, which occasionally cause the system to hang.
Hence the disk has been removed.
11.2 USB Resolution
Aha! I have finally worked out a solution to the problems
outlined above. This article
in the Linux FAQ provided the key, and I
have extended the ideas outlined in that article. The key
point is the fact that the port numbers allocated are ordered
in the order of the devices plugged into the hub. I am using
a 7-port VIA Labs Hub, and by keeping a list of the devices
and port numbers, I can use this together with the information
returned by udevadm to map devices to /dev/ttyUSB
device files. The page
Writing udev rules
also gives helpful advice.
That mapping is saved in a file
/home/ajh/etc/usbPorts.txt, which is generated by the
python program usbFind.py.
I have since discovered similar information resides in the
directory /dev/serial/by-path/. I record these
here for future use, but for now, things are working so I
won't change them! Here are the entries in that
directory:
lrwxrwxrwx 1 root root 13 Jul 10 11:59 pci-0000:00:14.0-usb-0:2.4:1.0-port0 -> ../../ttyUSB3
lrwxrwxrwx 1 root root 13 Jul 10 15:21 pci-0000:00:14.0-usb-0:2.1.2:1.0-port0 -> ../../ttyUSB1
lrwxrwxrwx 1 root root 13 Jul 11 11:52 pci-0000:00:14.0-usb-0:2.3:1.0-port0 -> ../../ttyUSB0
lrwxrwxrwx 1 root root 13 Jul 11 16:11 pci-0000:00:14.0-usb-0:2.2:1.0-port0 -> ../../ttyUSB2
Now for my mapping file. The columns are hub port number,
device name, USB number. Note that the last column is not
necessary (use dashes if not known), and is (re)computed by
the usbFind.py program.
"usbPorts.txt" 11.1 =01 - -
02 arduino 1
03 - -
04 - -
05 spare 2
06 tank 0
07 solar 3
Now for the usbFind.py program, which is written as a
module which provides a USBclass, with methods to
access and update the usbPorts.txt file. If called as
a main program, it does exactly that, and regenerates the
mapping file from scratch.
A key method is the get routine, which translates a
device name into a USB device port. This is utilised in the
getDevice program, installed as part of the bin library.
"usbFind.py" 11.2 =
11.2.1 usbFind: USBclass.load
<usbFind: USBclass.load 11.3> =def load(self):
plugfile=open(PORTFILE,'r')
plugno=1; portno=1
maps=[]
for line in plugfile.readlines():
line=line.strip()
res=re.match('^(\d\d) +(.{8}) *((\d|-))?$',line)
if res:
socket=res.group(1)
name=res.group(2)
usb=res.group(3)
if Debug: print socket,name,usb
if name[0]!='-':
if usb.isdigit():
maps.append((portno,plugno,name,int(usb)))
else:
maps.append((portno,plugno,name,'-'))
plugno+=1
else:
if Debug: print "could not match %s" % line
portno+=1
plugfile.close()
self.maps=maps
return
11.2.2 usbFind: USBclass.compute
<usbFind: USBclass.compute 11.4> =def compute(self):
udevop=[None for i in range(7)]
for usb in range(7):
cmd='/bin/udevadm info -a -n /dev/ttyUSB%d' % usb
cmd=cmd.split(' ')
if Debug: print cmd
try:
udevop[usb]=subprocess.check_output(cmd)
except:
print "Cannot get info for device /dev/ttyUSB%s" % (usb)
usb=0; portno=0; plugno=1
for op in udevop:
if Debug: print "looking at usb=%d, portno=%d, plugno=%d" % (usb,portno,plugno)
if not op:
usb+=1
continue
op=op.split('\n')
for l in op:
l=l.strip()
res=re.match('KERNELS=="2-2.(\d)"$',l)
if res:
plugno=int(res.group(1))
(port,plug,name,u)=self.maps[plugno-1]
if Debug:
print "found entry %s" % (l)
print "plug number %d is the %s and maps to /dev/ttyUSB%d" % \
(plugno,name,usb)
self.maps[plugno-1]=(port,plugno,name,usb)
portno+=1
usb+=1
return
11.2.3 usbFind: USBclass.device2port
<usbFind: USBclass.device2port 11.5> =def device2port(self,dev):
nlen=len(dev)
for i in range(len(self.maps)):
entry=self.maps[i][2][0:nlen]
if dev==entry:
return self.maps[i][3]
get=device2port # alias
11.2.4 usbFind: USBclass.save
<usbFind: USBclass.save 11.6> =def save(self):
portsfile=open(PORTFILE,'w')
j=0
for i in range(7):
#print i+1,self.maps[j]
if self.maps[j][0]==i+1:
(port,plug,name,usb)=self.maps[j]
portsfile.write("%02d %8s%1s\n" % (i,name,usb))
j+=1
else:
portsfile.write("%02d - -\n" % i)
portsfile.close()
return
"getDevice" 11.7 =#!/usr/bin/python
<edit warning 2.1>
import sys
from usbFind import *
usb=USBclass()
device=sys.argv[1]
usbNo=usb.get(device)
if usbNo==None:
print "Cannot get port for device %s" % (device)
else:
print "/dev/ttyUSB%1d" % (usbNo)
Call this routine as getDevice arduino, where
arduino may be replaced by some other device, as
listed in the table in <usbPorts.txt 11.1>
11.3 Current Patchboard Wiring
11.3.1 Cellar Computer Board
TOP ROW |
Terminal |
|
Left |
|
Middle |
|
Right |
|
|
|
name |
colour |
function |
|
|
name |
colour |
function |
|
|
name |
colour |
function |
|
|
1 |
|
gnd |
black |
|
|
|
|
|
|
|
|
|
black |
|
|
|
2 |
|
|
white |
|
|
|
|
|
|
|
|
|
white |
|
|
|
3 |
|
|
green |
|
|
|
|
|
|
|
|
|
green |
|
|
|
4 |
|
|
blue |
|
|
|
|
|
|
|
|
|
blue |
|
|
|
5 |
|
|
brown |
|
|
|
|
|
|
|
|
|
brown |
|
|
|
6 |
|
gnd |
black |
|
green cable |
|
|
|
|
|
|
gnd |
black |
|
|
|
7 |
|
|
red |
nc |
|
|
|
|
|
|
|
white |
RainForest |
|
|
8 |
|
|
yellow |
|
|
|
|
|
|
|
|
green |
|
|
|
9 |
|
|
brown |
heater aux relay coil |
|
|
|
|
|
|
|
blue |
TomatoesHanging |
|
|
10 |
|
|
white |
heater indicator |
|
|
|
|
|
|
|
brown |
|
|
|
11 |
|
|
purple |
Arduino heater relay |
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
- |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
BOTTOM ROW |
Terminal |
|
Left |
|
Middle |
|
Right |
|
|
|
name |
colour |
function |
|
|
name |
colour |
function |
|
|
name |
colour |
function |
|
|
1 |
|
|
|
|
|
|
I |
black |
ChookProve |
red cable |
|
gnd |
black |
|
red cable |
|
2 |
|
|
|
|
|
|
J |
white |
seedling |
|
A |
white |
|
|
3 |
|
|
|
|
|
|
H |
green |
|
|
G |
green |
Flush |
|
4 |
|
|
|
|
|
|
K |
blue |
|
|
B |
blue |
ChookUp |
|
5 |
|
|
|
|
|
|
|
brown |
|
|
C |
brown |
ChookDown |
|
6 |
|
|
|
|
|
|
|
|
|
|
|
gnd |
black |
|
red cable |
|
7 |
|
|
|
|
|
|
|
|
|
|
|
D |
white |
|
|
8 |
|
|
yellow |
|
9 strand cable
to side veg beds |
|
|
|
|
9 strand cable
to side veg beds |
|
|
green |
|
|
9 |
|
|
pink |
{fn} |
|
|
white |
CarportVegBed |
|
E |
blue |
Woo2Plas |
|
10 |
|
|
green |
MiddleVegBed |
|
|
red |
TopVegBed |
|
F |
brown |
FloodNDrain |
|
11 |
|
unused |
|
|
|
gnd |
black |
|
|
|
|
|
|
|
12 |
|
|
blue |
|
|
|
brown |
BottomVegBed |
|
|
|
|
|
|
These are all numbered top to bottom in each block.
11.3.2 Cellar Door Board
Terminal |
|
name |
colour |
cnx |
function |
|
1 |
|
gnd |
black |
(white) |
|
|
2 |
|
A |
white |
(black) |
|
|
3 |
|
G |
green |
flush |
|
|
4 |
|
B |
blue |
|
ChookUp |
|
5 |
|
C |
brown |
|
ChookDown |
|
6 |
|
gnd |
black |
|
|
|
7 |
|
D |
white |
(white) |
|
|
8 |
|
|
green |
|
|
|
9 |
|
E |
blue |
|
Woo2Plas |
|
10 |
|
F |
brown |
|
FloodNDrain |
|
11 |
|
I |
black |
|
ChookProve |
|
12 |
|
J |
white |
|
Seedling |
|
13 |
|
H |
green |
|
|
|
14 |
|
K |
blue |
|
|
|
These are all numbered top to bottom in each block.
11.3.3 House Corner Board
|
|
Far Left (1) |
|
Centre Left (2) |
|
Centre Right (3) |
|
Far Right (4) |
|
Terminal |
|
name |
colour |
cnx |
function |
|
name |
colour |
cnx |
function |
|
name |
colour |
cnx |
function |
|
name |
colour |
cnx |
function |
|
1 |
|
J |
|
nc |
|
|
H |
(white) |
1.3 |
|
|
gnd |
|
1.9 |
|
|
gnd |
|
|
|
|
2 |
|
K |
|
2.2 |
|
|
K |
|
1.2 |
|
|
A |
|
1.10 4.2 |
|
|
A |
|
1.9 |
+12v |
|
3 |
|
H |
|
2.1 |
|
|
spare |
|
|
|
|
G |
|
nc |
|
|
gnd |
|
|
|
|
4 |
|
I |
|
3.11 |
ChookProve |
|
spare |
|
|
|
|
B |
|
1.5 |
ChookUp |
|
spare |
|
|
|
|
5 |
|
B |
|
3.4 |
ChookUp |
|
spare |
|
|
|
|
C |
|
1.6 |
ChookDown |
|
gnd |
|
3.8 |
|
|
6 |
|
C |
|
3.3 |
ChookDown |
|
spare |
|
|
|
|
gnd |
|
|
|
|
|
blue |
3.9 |
Woo2Plas |
|
7 |
|
gnd |
|
3.8 |
|
|
spare |
|
|
|
|
D |
|
nc |
|
|
spare |
|
|
|
|
8 |
|
J |
|
3.12 |
Seedling |
|
spare |
|
|
|
|
gnd |
|
1.7 4.5 |
|
|
|
brown |
3.10 |
FloodNDrain |
|
9 |
|
gnd |
|
4.1 |
|
|
spare |
|
|
|
|
|
|
4.6 |
|
|
gnd |
black white |
|
|
|
10 |
|
A |
|
3.2 |
|
|
spare |
|
|
|
|
|
|
|
|
|
gnd |
|
|
|
|
11 |
|
spare |
|
|
|
|
spare |
|
|
|
|
I |
|
1.4 |
|
|
spare |
|
|
|
|
12 |
|
spare |
|
|
|
|
spare |
|
|
|
|
N |
|
1.8 |
|
|
spare |
|
|
|
|
These are all numbered top to bottom within each block.
11.3.4 BBQ Board
Terminal |
|
name |
colour |
cnx |
function |
Comment |
Cable |
|
1 |
|
J |
|
|
|
|
|
|
2 |
|
K |
|
|
|
|
|
|
3 |
|
H |
(white) |
nc |
|
|
|
|
4 |
|
I |
yellow |
|
|
|
|
|
5 |
|
B |
blue.Chook |
|
ChookUp |
towards sky |
Chook Cable |
|
6 |
|
C |
brown.Chook |
|
ChookDown |
towards earth |
|
7 |
|
gnd |
|
|
|
|
|
8 |
|
J |
|
|
|
|
|
9 |
|
gnd |
black.Chook |
|
-12v (gnd) |
|
|
10 |
|
A |
white.Chook |
|
+12v |
|
|
11 |
|
|
|
|
unused |
|
|
12 |
|
|
|
|
unused |
|
|
These are numbered left to right.
11.3.5 Restructure Plan
There are some photos of the current state of play, which I
will post here soon. In the meantime, there are plans to
restructure the relay wiring, using the following colour
coding:
colour |
circuit |
red |
blue |
white |
green |
yellow |
orange |
purple |
heating |
grey |
brown |
I have to admit, I'm not entirely sure this is the best idea ...
11.3.6 Power Bus
Given the frequency with which alterations are made to the
control panel, and the need for power sources without
interrupting other devices, it is suggested that the power rails
be split along the following lines:
BeagleBone |
5v |
|
|
4-way Relay |
5v |
|
|
8-way Relay |
5v |
|
|
Arduino |
5v |
|
|
Lights |
|
12v |
|
Heater Lamp |
|
12v |
|
Weather Station |
|
12v |
|
Solenoids |
|
|
25vac |
Variations on this are possible - for example, combining
the 4- and 8-way relay supplies.
11.4 Current Cat5 Ethernet Wiring
There are n switches around the house, known by a single
letter name. These are:
D |
Desktop (in Study) |
F |
Family Room |
J |
Jolimont: 24 port 100Mb switch in
cellar |
M |
Middle Bedroom |
P |
Patch Panel: 24 port. Not strictly a switch! |
S |
Study |
|
|
|
|
|
|
|
|
|
|
|
|
11.4.1 Cellar Patch Board
There are two components: a 24 port patch panel, and
a 24 port 100Mb switch.
Firstly, the patch panel P: |
Now the switch (Netgear 24-port 1000Mbs) J:
|
Port |
Name |
Plugged |
Purpose |
1 |
|
|
|
2 |
|
|
|
3 |
|
|
|
4 |
Study Wallplate Left |
J4 |
uplink |
5 |
|
|
|
6 |
|
|
|
7 |
|
|
|
8 |
|
|
|
9 |
Master Bed Right |
J9 |
|
10 |
Master Bed Left |
|
|
11 |
|
|
not used |
12 |
|
|
|
13 |
Family Vacuum |
J13 |
F switch (F1) |
14 |
Dining Left |
J14 |
|
15 |
Dining Right |
J15 |
|
16 |
Lounge |
J16 |
not wired (?) |
17 |
|
|
|
18 |
|
|
|
19 |
|
|
|
20 |
|
|
|
21 |
|
|
|
23 |
Cellar Trains Left |
J23 |
|
24 |
Cellar Lilydale |
J24 |
|
|
Port |
Name |
Plugged |
Purpose |
1 |
|
|
|
2 |
|
|
|
3 |
|
|
|
4 |
Study Wallplate Left |
P4 |
uplink |
5 |
|
|
|
6 |
|
|
|
7 |
|
|
|
8 |
|
|
|
9 |
Master Bed Right |
P9 |
|
10 |
Master Bed Left |
P10 |
|
11 |
Family Door Left |
|
TV HDMI |
12 |
Family Door Right |
|
|
13 |
Family Vacuum |
P13 |
F switch |
14 |
Dining Left |
P14 |
|
15 |
Dining Right |
P15 |
|
16 |
Lounge |
P16 |
not wired (?) |
17 |
|
|
|
18 |
|
|
|
19 |
|
|
|
20 |
|
|
|
21 |
|
|
|
23 |
Cellar Trains Left |
P23 |
|
24 |
Cellar Lilydale |
P24 |
CHECK THIS! |
|
Note that most of these are direct connexions to the
corresponding patch panel port.
11.4.2 Study
There are two switches located in the study: a Netgear
DGS1008D 8-port 1000Mbs (known a S, Study), and a Netgear
ProSafe 5-port 1000Mbs (known as D, Desktop)
There are also 2 wall plates, each with 2 ports,
known as L1, L2, R1 R2 (Left and Right)
Netgear DGS1008D 8-port 1000Mbs
Port |
Name |
Plugged |
Purpose |
1 |
Desktop Link |
D1 |
downlink |
2 |
|
|
spare |
3 |
Study Wall Plate |
L2 |
|
4 |
TV Computer |
|
Link to Bed 2, TV Computer (via window!) |
5 |
Laser Printer |
|
|
6 |
NAS |
|
(unused) |
7 |
Billion Modem |
|
|
8 |
Efergy Receiver |
|
|
Netgear ProSafe 5-port 1000Mbs
Port |
Name |
Plugged |
Purpose |
1 |
Study Switch |
S1 |
uplink |
2 |
|
|
spare |
3 |
Study Wall Plate |
R1 |
|
4 |
|
|
spare |
5 |
Wolseley |
|
|
Study Wall Plates
Port |
Name |
Plugged |
Purpose |
L1 |
|
R2 |
|
L2 |
Cellar Patch Panel P4 |
S3 |
downlink |
R1 |
Study Wall Plate |
|
|
R2 |
|
L1 |
|
11.4.3 Family
Netgear 8-port 1000Mbs
Port |
Name |
Plugged |
Purpose |
1 |
Family Switch |
Family Vacuum |
F1 |
2 |
|
|
spare |
3 |
|
|
spare |
4 |
Dimboola |
|
Dimboola |
5 |
Laptop |
|
(if used) |
6 |
|
|
spare |
7 |
|
|
spare |
8 |
|
|
spare |
12. Test Programs
12.1 Check RPC Operation
The following short fragment of code is intended to check the
operation of the RPC mechanisms on both garedelyon and
lilydale. It provides the user with one RPC object, o
(bastille), which can
be used to invoke the RPC interfaces. Several such
(information supply only) interfaces are invoked as
examples.
Usage is to import this code into an interpretive invocation
of python, viz from testRPC import *.
"testRPC.py" 12.1 =
<edit warning 2.1>
import xmlrpclib
from HouseDefinitions import CENTRAL
s=xmlrpclib.ServerProxy('http://%s:8001' % CENTRAL)
print "options are:"
print " s.getState()"
print " s.setState([0,0,...]) # 12-element vector of 0/1"
print " s.setBitOn(bitnumber) # bit number is an integer (0-11)"
print " s.setBitOff(bitnumber) # bit number is an integer (0-11)"
print
#print " s.getHeating()"
#print " s.setHeating(float,'on'/'off')"
print " s.getSolar(n) # n is register number (32 is input amps)"
#print " s.getTemps()"
#print " s.getTank()"
#print " s.maxminTemp()"
print
print "for example,"
#print " s.getHeating()=%s" % (s.getHeating())
print " s.getState()=%s" % (s.getState())
print
Note that the data logging operations are not currently
available.
13. The Log Files
Here is a summary of all the log files maintained:
- RelayServer.log
-
lilydale:/Users/ajh/logs/RelayServer.log
logs activity of the relay controller, recording relay sets
and resets.
- cron.log
-
bastille:/home/ajh/Computers/House/cron.log
logs behaviour of the AdjustHeat.py program,
responsible for adjusting the heating on or off, depending upon
the current temperature and the desired demand temperature.
(This should probably be moved into the logs directory and
renamed to heatadjust.log)
- housedata.log
-
garedelyon:/logdisk/logs/housedata.log
logs calls on the garedelyon RPC server, along with some
debugging information (which should probably be removed).
- maxmins.log
-
garedelyon:/logdisk/logs/maxmins.log
logs the maximum and minimum outside temperatures for the last
8 days.
- relay.log
- garedelyon:/logdisk/logs/relay.log
- solar.log
- garedelyon:/logdisk/logs/solar.log
- tank.log
- garedelyon:/logdisk/logs/tank.log
- temp.log
- garedelyon:/logdisk/logs/temp.log
14. (Re)Starting the Server
14.1 Introduction
This has been a somewhat fraught area of development. Issues
with identifying what USB ports are connected to what devices,
Arduino startup issues, and just plain knowing the current
state of the system is when starting various services, have
made this area less than reliable. However, steady progress
has been made, and currently the startup scripts are being
moved to utilise the
UpStart software used to manage all Ubuntu services.
This is a list of all the processes that need
starting:
- usb - to ensure that all USB connexions are identified
correctly
- flask - to provide a web server and house interface
- arduino - to control the relay drivers
- relayserver - to interface to the Arduino
- tank logging - to record tank levels
- solar logging - to record insolation
- weather station - to provide temperature data
- chook door
14.2 Non-Upstart Method (more reliable!)
14.2.1 Sort out USB allocations
The USB allocations change on each reboot, and anytime that
a USB plug is removed or added, or the USB hub power
cycled. Hence when restarting the house system, it is wise
to make sure that all USB allocations are known. A script
called usbFind.py checks all 7 ports of the hub, and
writes the allocations into the file
/home/ajh/etc/usbPorts.txt.
$ + /home/ajh/Computers/House
$ usbFind.py
$ -
Check the file /home/ajh/etc/usbPorts.txt. It
should look like this:
00 - -
01 arduino 0
02 - -
03 - -
04 wx200 1
05 tank 2
06 solar 3
14.2.2 Start the Flask Server
Starting the flask server is simple:
$ + /home/ajh/Computers/House
$ startFlask.sh
$ -
Do not worry if you get a message saying that such-and-such
process does not exist. That simply reflects the fact that
the previous flask server has been killed in some way (for
eaxmple, by a reboot).
14.2.3 Start the Arduino Driver
Usually straightforward.
$ + /home/ajh/Computers/House/RelayDriver8way
$ . setupMake.sh
$ make -f arduino.mk upload
$ -
Check that the USB port used matches that identified in the
/home/ajh/etc/usbPorts.txt file.
If this process appears to fail, you can check that the
Arduino is working by manually sending it a relay state word:
echo 001000000000 >`getDevice arduino`
You should see the relay state change.
14.2.4 Start the RelayServer
Should be straightforward:
$ + /home/ajh/Computers/House
$ startRelayServer.sh
$ -
Again, you can ignore messages about non-existent processes.
14.2.5 Start the Tank Logger
$ + /home/ajh/Computers/House
$ startTankLog.sh
$ -
The USB port is automatically determined by the
startTankLog.sh script. Ignore missing process
messages.
14.2.6 Start the Solar Logger
This script is slightly different, as the regular logging
is performed by the EveryMinute.sh script, which runs
once a minute. Until the USB device is discovered
automatically, you need to check the pl60.c script in
/home/ajh/Computers/House/pl60-ubuntu-1.0/ uses the
correct USB port. Recompile with the correct value (make
pl60) and copy the result into the bin directory, so
that EveryMinute uses the correct program.
14.2.7 Start the Weather Station Daemon
The weather station uses a daemon to read the weather
station data, and make it available to the user level
scripts. The daemon is started with:
$ + /home/ajh/Computers/House/wx200d-ubuntu-1.1
$ wx200d -s /dev/ttyUSBn
where n is the usb number given by the
/home/ajh/etc/usbPorts.txt file. Check that this is
working correctly by running
$ wx200
$ -
which should display a heap of weather information. If it
doesn't, try power cycling the weather station itself (pull
out the power cable, wait a few seconds, and replace it).
You will need to reset the date and time, and units.
14.2.8 Start the Chook Door Server
The chook door server provides an RPC interface to
interrogate whether or not the chook door is closed,
according to the proving microswitch. This circuit is
independent of the actual closing and opening process, and
provides a safety check that the door is actually closed,
once the close command has been issued, and the door has had
time to close. It runs continuously on a BeagleBone server
(auteuil), hence:
auteuil $ python /home/ajh/Computers/House/ChookDoorServer.py
Since this program runs continuously, it should be started
in a separate terminal window, which is then hidden from
view, but left running.
14.2.9 Start the Chook Door Prover
The chook door prover is a small program that runs
continuously on a BeagleBone server (auteuil), and
monitors the state of the proving microswitch. It drives a
pair of leds to indicate the state, and also writes the
current state to a file status. It must run as root,
hence to start it:
auteuil $ sudo bash
auteuil # python /home/ajh/Computers/House/ChookProve.py
Again, this program outputs the status everytime it
changes, so it should be started in a terminal window which
is then minimized and left running.
14.3 The New UpStart Configuration
Like its Unix counterpart init.d, the init
directory contains scripts to start and stop each component of
the HouseMade system.
Here is a list of the relevant files:
- /etc/init/chookDoor.conf
- /etc/init/flask.conf
- /etc/init/relayserver.conf
- /etc/init/usbPorts.conf
The upstart software requires that startup scripts are
located in /etc/init. This is where the following
startup scripts have currently been placed for testing
purposes, but the intention is to move them to a separate user
area.
"house.conf" 14.1 =#!/bin/bash
start usb
start flask
The different services provided are outlined in the
following subsections.
14.3.1 USB Allocation
Run the command
$ start usbPorts
This generates a list of the USB devices found in port order
in the attached 7-port USB Hub, and identifying, for each list
tuple:
- Hub socket number;
- USB sequence number;
- device name;
- USB device
So, for example, this invocation records that the Solar
controller is the last USB cable in the hub, is plugged into
hub socket number 7, and that it has been allocated to
/dev/ttyUSB3.
These values are recorded in the file
~/etc/usbPorts.txt and are used by subsequent
programs in the startup sequencing.
"usb.conf" 14.2 =#!/bin/bash
USBCONF='/home/ajh/etc/usbPorts.txt'
HOUSE='/home/ajh/Computers/House'
PYTHON='/usr/bin/python'
COMMAND=$1
case $COMMAND:
stop)
# nothing to do
;;
start | restart)
${PYTHON} ${HOUSE}/usbFind.py
;;
14.3.2 Flask Server
Run the command
$ start flask
It normally generates no output, unless the previous
invocation failed in some way.
"flask.conf" 14.3 =#!/bin/bash
LOGDIR='/home/ajh/logs/central'
HOUSE='/home/ajh/Computers/House'
COMMAND=$1
# first check if anything needs to be stopped
if [ -f ${LOGDIR}/flaskProcess ] ; then
for p in `cat ${LOGDIR}/flaskProcess` ; do
kill -9 $p
done
rm ${LOGDIR}/flaskProcess
fi
case $COMMAND:
stop)
# already done
;;
start | restart)
${HOUSE}/lilydaleFlask.py >>${LOGDIR}/flask.log 2>&1 &
ps aux | grep "Flask.py" | grep -v grep | \
awk '{print $2}' >${LOGDIR}/flaskProcess
;;
14.3.3 Arduino
Start the Arduino. Best to do this while in the Arduino
subdirectory, so cd to that:
+ ~/Computers/House/RelayDriver8way
Once there, you need to know what device port the Arduino is
connected. The getDevice arduino script is used to
do this, and is invoked by the Arduino loader script
/home/ajh/Computers/House/RelayDriver8way/startArduino.sh
Run this script to ensure the Arduino is correctly loaded (in
principle, not necessary as the program is in flash memory),
and then check that the Arduino is correctly running by
issuing
echo 001000000000 >`getDevice arduino`
This should set the third leftmost relay (unused) if the
device is working correctly. If it isn't, try running
make monitor, and sending 12-bit sequences at
it. Quit with C-A \ y and then try the echo instruction
again. If that doesn't work, and reports device busy, then
kill -9 `fuser /dev/ttyUSBn` where n is the USB
number reported as busy. After this, you are on your own.
No, not quite. If you find that the device is locked, look
in directory /run/lock/ for a lock file LCK..
that matches your locked device. Removing this file will
generally fix the locked device.
14.3.4 Relay Server
Start the Relay Server (first go back to the House
directory, do a - to get there):
startRelayServer.sh
14.3.5 Tank Logging
Start the tankLogging:
startTankLog.sh
Again, the device port needs to be known. This is currently
provided by the script /home/ajh/bin/getDevice tank,
which is called by the startTankLog.sh script.
14.3.6 Solar Logging
Ensure the solarLogging is running. This requires that
the C code routine pl60.c is compiled and placed in the
home bin directory (/home/ajh/bin/) so that it can be
executed by the solar.py module when called by the
EveryMinute.sh script.
14.3.7 Weather Station
The weather code relies upon the weather station daemon,
wx200d. Firstly, make sure that the weather station
port is correctly assigned:
$ getDevice wx
/dev/ttyUSB1
(Note that device may be slightly different.) Then check
that the device alias is correctly set:
$ ll /dev/wx200
lrwxrwxrwx 1 root root 7 Dec 17 16:43 /dev/wx200 -> ttyUSB1
Note that the device linked to should agree with the
response from the getDevice wx command. If it
is not, it will need to be updated. (Use sudo, and document
this at some future stage.)
Once this is in place, start the daemon itself with
$ /home/ajh/Computers/House/wx200d-1.1/wx200d
Check the operation of the weather station by running
$ /home/ajh/Computers/House/wx200d-1.1/wx200
which should give a display of all weather station variables.
14.3.8 Regular Service Activity
15. The Cron Jobs
See the cron.xlp
file for the actual code. There is just the single interface to
cron: the script EveryMinute.sh which is run once a
minute.
"EveryMinute.sh" 15.1 =#!/bin/bash
<edit warning 2.1>
# collect all the jobs that need to be run regularly into one place
BIN=/home/ajh/bin
HOUSE=/home/ajh/Computers/House
LOGS=/home/ajh/logs/central
PERSONAL=/home/ajh/www/personal
HOMESERVER=ajh.co:www
export HOST=lilydale
TZ='Australia/Melbourne'; export TZ
export PYTHONPATH=.:/home/ajh/lib/python:/home/ajh/Computers/python
export TERM=vt100
# these all done every minute
/usr/bin/python ${HOUSE}/checkChooks.py >>${LOGS}/chook.log 2>&1
/usr/bin/python ${HOUSE}/ChookDoor.py --compute >>${LOGS}/chook.log
/usr/bin/python ${HOUSE}/logsolar.py >>${LOGS}/solar.log
/usr/bin/python ${HOUSE}/logwx.py >>${LOGS}/temp.log
/usr/bin/python ${HOUSE}/AdjustHeat.py >>${LOGS}/heating.log
tail -1 ${LOGS}/tank.log >${LOGS}/tankState
# these only done every 5 minutes
minutes=`/bin/date "+%M"` # get current minute
five=`expr $minutes % 5` # get minute mod 5
if [ $five -eq 0 ] ; then
#/usr/bin/python ${HOUSE}/AdjustHeat.py >>${LOGS}/heating.log
/usr/bin/python ${HOUSE}/tempplot.py
/usr/bin/python ${HOUSE}/solarplot.py
/usr/bin/python ${HOUSE}/tankplot.py
/usr/bin/rsync -auv ${PERSONAL}/tempplot.png ${HOMESERVER}/personal/
/usr/bin/rsync -auv ${PERSONAL}/solarplot.png ${HOMESERVER}/personal/
/usr/bin/rsync -auv ${PERSONAL}/tankplot.png ${HOMESERVER}/personal/
/usr/bin/rsync -auv ${PERSONAL}/tankplot7.png ${HOMESERVER}/personal/
# these two commented out due to some funny root mode incompatibility
#/bin/bash ${HOUSE}/checkFlask.sh
#${BIN}/watchFlask.sh >>${LOGS}/flaskCheck.log
fi
Note that this script gets executed every minute, as the name
suggests. However, some tasks do not need such frequent service,
and hence the calculation of minutes mod 5 so that the
adjustHeating is only run every 5 minutes.
"Makefile" 1.1 =default=HouseMade
GenFiles = install-HouseMade HouseData.py tanklogging.c
CGI = ${HOME}/www/cgi-bin
HOUSE = ${HOME}/Computers/House
BIN = ${HOME}/bin
AUTEUIL = auteuil:${HOME}/Computers/House
BASTILLE = bastille:${HOME}/Computers/House
LILYDALE = lilydale:${HOME}/Computers/House
WEBSOURCE = ${HOME}/www/computing/sources/house
XSLLIB = /home/ajh/lib/xsl
XSLFILES = $(XSLLIB)/lit2html.xsl $(XSLLIB)/tables2html.xsl
include ${HOME}/etc/MakeXLP
${GenFiles}:HouseMade.tangle
install: install-flask install-definitions install-house \
install-timer install-startFlask
HouseMade.tangle HouseMade.xml: HouseMade.xlp Makefile.xlp cat5cabling.xlp \
WebInterface.xlp KeyDataStructures.xlp \
RelayControl.xlp patchboards.xlp Makefile.xlp
xsltproc --xinclude $(XSLLIB)/litprog.xsl HouseMade.xlp >HouseMade.xml
touch HouseMade.tangle RelayControl.tangle Makefile.tangle
HouseMade.html: HouseMade.xml $(XSLFILES)
xsltproc --xinclude $(XSLLIB)/lit2html.xsl HouseMade.xml >HouseMade.html
html: HouseMade.html
pdf: HouseMade.pdf
##############################
# INSTALL LILYDALE CODE #
##############################
# install flask modules
# keep these arranged alphabetically
install-AdjustHeat: HouseMade.tangle
scp AdjustHeat.py ${LILYDALE}/
touch install-AdjustHeat
install-arduino: RelayControl.tangle
scp getArduinoUSB.sh lilydale:/home/ajh/bin/
scp startArduino.sh ${LILYDALE}/
scp setupMake.sh ${LILYDALE}/
touch install-arduino
install-chook: HouseMade.tangle
scp ChookProve.py ${AUTEUIL}/
scp ChookDoorServer.py ${AUTEUIL}/
install-definitions: HouseMade.tangle
scp HouseDefinitions.py ${LILYDALE}/
touch install-definitions
install-everyMinute: HouseMade.tangle
chmod 755 EveryMinute.sh
scp EveryMinute.sh ${LILYDALE}/
touch install-everyMinute
install-flask: HouseMade.tangle install-house install-timer install-definitions install-watchFlask
chmod 755 lilydaleFlask.py
scp lilydaleFlask.py ${LILYDALE}/
touch install-flask
install-house: HouseMade.tangle install-definitions
scp HouseMade.py ${LILYDALE}/
touch install-house
install-html: HouseMade.html
if /usr/bin/diff HouseMade.html ${WEBSOURCE}/HouseMade.html; then \
cp HouseMade.html ${WEBSOUCE}/HouseMade.html ;\
fi
install-relay: RelayControl.tangle
chmod 755 RelayServer.py
scp RelayServer.py RelayControl.py ${LILYDALE}/
scp testRPC.py ${LILYDALE}/
touch install-relay
install-startFlask: HouseMade.tangle
chmod 755 startFlask.sh
scp startFlask.sh ${LILYDALE}/
touch install-startFlask
install-startRelay: HouseMade.tangle install-relay
chmod 755 startRelayServer.sh
scp startRelayServer.sh ${LILYDALE}/
touch install-startRelay
install-tank: HouseMade.tangle
scp tank.py ${LILYDALE}/
scp tankplot.py ${LILYDALE}/
scp startTankLog.sh ${LILYDALE}/
touch install-tank
install-timer: HouseMade.tangle
scp TimerModule.py ${LILYDALE}/
touch install-timer
install-watchFlask: HouseMade.tangle
scp watchFlask.py.py ${LILYDALE}/../../bin/
echo "Check sudo sticky bit on watchFlask.py"
ssh lilydale ls -l ${LILYDALE}/../../bin/watchFlask.py
touch install-watchFlask
start-auteuil: install-chook
ssh root@auteuil chmod 4711 ${HOUSE}/ChookProve.py
ssh auteuil /usr/bin/python ${HOUSE}/ChookProve.py
ssh auteuil /usr/bin/python ${HOUSE}/ChookDoorServer.py
start-lilydale: start-flask start-relay start-arduino
touch start-lilydale
start-flask: install-startFlask install-flask
ssh lilydale ${HOUSE}/startFlask.sh
touch start-flask
start-relay: install-startRelay
ssh lilydale ${HOUSE}/startRelayServer.sh
touch start-relay
start-arduino: install-arduino
ssh lilydale ${HOUSE}/startArduino.sh
touch start-arduino
start-tank: install-tank
ssh lilydale ${HOUSE}/startTankLog.sh
touch start-tank
<Makefile: install bastille 1.2> **** Chunk omitted!
###########################
# MAKEFILE #
###########################
makefile: Makefile.tangle
###########################
# LEGACY CODE #
###########################
install-flinders: HouseMade.tangle executable
rsync -auv house.py timer.py flinders:${CGI}/
rsync -auv checkTime.py flinders:${HOUSE}/
rsync -auv tank.py flinders:${HOME}/lib/python/
rsync -auv tank.py flinders:${HOUSE}/
rsync -auv HouseDefinitions.py flinders:${HOUSE}/
rsync -auv TempPlot.py flinders:${HOUSE}/
touch install-flinders
install-wolseley: HouseMade.tangle executable
rsync -auv house.py timer.py wolseley:${CGI}/
rsync -auv checkTime.py wolseley:${HOUSE}/
rsync -auv tank.py wolseley:${HOME}/lib/python/
rsync -auv tank.py wolseley:${HOUSE}/
rsync -auv HouseDefinitions.py wolseley:${HOUSE}/
rsync -auv TempPlot.py flinders:${HOUSE}/
touch install-wolseley
######################
# INSTALL BY PROGRAM #
######################
install-tank.py: HouseMade.tangle
rsync -auv tank.py ${HOME}/lib/python/
rsync -auv tank.py garedelyon:/home/ajh/lib/python/
rsync -auv tank.py flinders:/home/ajh/lib/python/
rsync -auv tank.py flinders:/home/ajh/Computers/House/
#############################
# END OF INSTALLATION STUFF #
#############################
executable: house.py timer.py
chmod 755 house.py timer.py
all: HouseMade.html
clean: litclean
-rm $(GenFiles)
16.1 Makefile: install bastille
<Makefile: install bastille 1.2> =##############################
# INSTALL BASTILLE CODE #
##############################
# install flask modules
# keep these arranged alphabetically
install-AdjustHeat: HouseMade.tangle
scp AdjustHeat.py ${BASTILLE}/
touch install-AdjustHeat
install-arduino: RelayControl.tangle
scp getArduinoUSB.sh bastille:/home/ajh/bin/
scp startArduino.sh ${BASTILLE}/
scp setupMake.sh ${BASTILLE}/
touch install-arduino
install-definitions: HouseMade.tangle
scp HouseDefinitions.py ${BASTILLE}/
touch install-definitions
install-everyMinute: HouseMade.tangle
chmod 755 EveryMinute.sh
scp EveryMinute.sh ${BASTILLE}/
touch install-everyMinute
install-flask: HouseMade.tangle install-house install-timer install-definitions
chmod 755 bastilleFlask.py
scp bastilleFlask.py ${BASTILLE}/
touch install-flask
install-house: HouseMade.tangle install-definitions
scp HouseMade.py ${BASTILLE}/
touch install-house
install-html: HouseMade.html
if /usr/bin/diff HouseMade.html ${WEBSOURCE}/HouseMade.html; then \
cp HouseMade.html ${WEBSOUCE}/HouseMade.html ;\
fi
install-relay: RelayControl.tangle
chmod 755 RelayServer.py
scp RelayServer.py RelayControl.py ${BASTILLE}/
scp testRPC.py ${BASTILLE}/
touch install-relay
install-startFlask: HouseMade.tangle
chmod 755 startFlask.sh
scp startFlask.sh ${BASTILLE}/
touch install-startFlask
install-startRelay: HouseMade.tangle install-relay
chmod 755 startRelayServer.sh
scp startRelayServer.sh ${BASTILLE}/
touch install-startRelay
install-tank: HouseMade.tangle
scp tank.py ${BASTILLE}/
scp tankplot.py ${BASTILLE}/
scp startTankLog.sh ${BASTILLE}/
touch install-tank
install-timer: HouseMade.tangle
scp TimerModule.py ${BASTILLE}/
touch install-timer
start-bastille: start-flask start-relay start-arduino
touch start-bastille
start-flask: install-startFlask install-flask
ssh bastille ${HOUSE}/startFlask.sh
touch start-flask
start-relay: install-startRelay
ssh bastille ${HOUSE}/startRelayServer.sh
touch start-relay
start-arduino: install-arduino
ssh bastille ${HOUSE}/startArduino.sh
touch start-arduino
start-tank: install-tank
ssh bastille ${HOUSE}/startTankLog.sh
touch start-tank
17. Document History
20140108:175940 |
ajh |
0.0 |
first draft, hacked from initial code development
scraped from Nathan's code. |
20140118:121534 |
ajh |
0.1 |
Substantial revisions |
20140124:105258 |
ajh |
0.2 |
More substantial revisions |
20140128:185934 |
ajh |
0.3 |
added relay control section, and started on solar
logging |
20140129:184058 |
ajh |
0.4 |
completed solar logging, and working on max/min
functionality |
20140220:160713 |
ajh |
0.5 |
extended relay control to 12 relays |
20140222:180919 |
ajh |
0.5.1 |
reshuffle some code, particular wrt moving heating
control to lilydale |
20140425:104834 |
ajh |
0.5.2 |
move lilydale functions to ringwood |
20140426:173322 |
ajh |
0.5.3 |
various tweaks to get ringwood working, update
Makefile |
20140427:161124 |
ajh |
0.5.4 |
add buttons to control watering |
20141123:105122 |
ajh |
0.5.5 |
make max min table more robust with key ckeck |
20141130:162421 |
ajh |
0.6.0 |
move relay timing functions into RelayServer (ringwood)
|
20141206:123633 |
ajh |
0.6.1 |
fix bug about timer not turning off |
20141206:181650 |
ajh |
0.6.2 |
rewrite RelayControl and re-organize RelayServer |
20150325:095450 |
ajh |
0.6.3 |
move ringwood functions back to lilydale |
20150405:125114 |
ajh |
1.0.0 |
Rescoped version for orsay |
20150419:151754 |
ajh |
1.0.1 |
working HouseMade and TimerModule included |
20150424:174421 |
ajh |
1.0.2 |
revise setBit operations, tidy documentation |
20150426:113730 |
ajh |
1.1.0 |
add Hardware Section |
20150507:215854 |
ajh |
1.1.1 |
add solar logging code back |
20150510:104116 |
ajh |
1.1.2 |
revised TimerModule to include timerData class, and
migrate to 5 minute programming quantum. |
20150531:122305 |
ajh |
1.1.3 |
|
20150606:115931 |
ajh |
1.1.4 |
rescoped for bastille |
20150628:215955 |
ajh |
1.1.5 |
add chook door display |
20150711:143257 |
ajh |
1.1.6 |
revised tank logging |
20150712:135643 |
ajh |
1.2.0 |
Removed several separate files, as they were getting
in the way, rather than helping. Revamped everything in
terms of referring to the house computer as "central",
rather than its actual hostname. This is apropos of the
Beaglebone bastille dying, and being replaced by
lilydale. Also added the usbFind code and
explanations. |
20150722:163546 |
ajh |
1.3.0 |
weather station now operational. Main change is to
rewrite the TimerModule into the HeatingModule, but other
changes are also underway. The web interface has been updated
to show temperature data. |
20150726:195116 |
ajh |
1.3.1 |
setup weather, solar and tank interfaces, allowing
separate flask interfaces |
20150729:114603 |
ajh |
1.3.2 |
add the currentState.py routine |
20150731:082907 |
ajh |
1.3.3 |
generalize thermostat initialization |
20150816:155707 |
ajh |
1.3.4 |
add section on UpStart operation |
20160131:154105 |
ajh |
1.3.5 |
add restart of chook door proving |
20160204:171508 |
ajh |
1.3.5 |
add cat5 cabling documentation |
<current version 16.1> = 1.3.6
<current date 16.2> = 20160204:171508
18. Indices
18.1 Files
File Name |
Defined in |
AdjustHeat.py |
5.1 |
ChookDoor.py |
8.1 |
ChookDoorServer.py |
8.8 |
ChookProve.py |
8.7 |
EveryMinute.sh |
15.1 |
HeatingModule.py |
3.20 |
HouseData.py |
9.2 |
HouseDefinitions.py |
2.2 |
HouseMade.sh |
3.12 |
Makefile |
1.1 |
RelayControl.py |
10.1 |
RelayServer.py |
10.2 |
TempLog.py |
5.3 |
checkChooks.py |
8.6 |
checkTime.py |
5.2 |
currentState.py |
9.1 |
flask.conf |
14.3 |
getDevice |
11.7 |
house.conf |
14.1 |
lilydaleFlask.py |
3.1 |
logsolar.py |
7.1 |
logwx.py |
4.2 |
setupMake.sh |
10.14 |
solar.py |
7.2 |
startArduino.sh |
10.13 |
startFlask.sh |
3.10 |
startHouseData.sh |
9.5 |
startRelayServer.sh |
10.12 |
startTankLog.sh |
6.1 |
tank.py |
6.2 |
testRPC.py |
12.1 |
usb.conf |
14.2 |
usbFind.py |
11.2 |
usbPorts.txt |
11.1 |
watchFlask.py |
3.11 |
wx200.py |
4.1 |
18.2 Chunks
Chunk Name |
Defined in |
Used in |
ChookDoor: compute |
8.3 |
8.1 |
ChookDoor: compute ephemeral values |
8.5 |
8.3 |
ChookDoor: load |
8.2 |
8.1 |
ChookDoor: save |
8.4 |
8.1 |
Flask: check what kind of file is requested |
3.4 |
3.3 |
Flask: collect parameters for XSLT translation |
3.7 |
3.3 |
Flask: compute the current working directory |
3.5 |
3.3 |
Flask: define the showpage routine |
3.3 |
3.1 |
Flask: define the various flask URLs and calls |
3.2 |
3.1 |
Flask: handle page counter information |
3.6 |
3.3 |
Flask: process the XSLT translation |
3.8 |
3.3 |
Flask: save html and process any errors |
3.9 |
3.3 |
HouseData define getTemps |
9.3 |
9.2 |
HouseData define maxminTemp |
9.4 |
9.2 |
HouseMade: check client connection |
3.17 |
3.12 |
HouseMade: collect date and time data |
3.16 |
3.12, 3.20
|
HouseMade: define the Generate Solar Data routine |
3.14 |
3.12 |
HouseMade: define the Generate Tank Data section |
3.15 |
3.12 |
HouseMade: define the Generate Weather Data routine |
3.13 |
3.12 |
HouseMade: generate the web page content |
3.18 |
3.12 |
Makefile: install bastille |
1.2 |
1.1 |
Web: define the heatingData class |
3.21 |
3.20 |
Web: heating: build widths for web page table |
3.23 |
3.20 |
Web: heating: collect parameters and update |
3.22 |
3.20 |
check client IP address OK |
3.24 |
|
current date |
16.2 |
|
current version |
16.1 |
|
house make temperature panel |
3.19 |
|
relayserver: countDown |
10.11 |
10.2 |
relayserver: define getTank |
10.3 |
10.2 |
relayserver: getState |
10.5 |
10.2 |
relayserver: getTimer |
10.4 |
10.2 |
relayserver: setBit |
10.7 |
10.2 |
relayserver: setBitOff |
10.9 |
10.2 |
relayserver: setBitOn |
10.8 |
10.2 |
relayserver: setState |
10.6 |
10.2 |
relayserver: start |
10.10 |
10.2 |
usbFind: USBclass.compute |
11.4 |
11.2 |
usbFind: USBclass.device2port |
11.5 |
11.2 |
usbFind: USBclass.load |
11.3 |
11.2 |
usbFind: USBclass.save |
11.6 |
11.2 |
18.3 Identifiers