HouseMade - The Hurst HouseHold Heater Helpmate
A.J.Hurst
Version 3.0.4
20201229:133233
The EventServer version has now been merged with this code.
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 Beagle Bone
System (operational: 10.0.0.20:9999)
-
The hardware interface to the relay drivers.
-
The Relay Server
(operational: CENTRAL:8001)
-
Provides an RPC interface to controlling the house relays.
-
The Event Server
(operational: <EventServerRPCaddress 8.2>)
-
Provides an RPC interface to controlling the various house
events.
-
The Web Interface
(operational)
-
Provides an easy to use interface to the program suite,
using two key programs: house and timer (?
check). These invoke the house computer.
-
The Chook Door System
(operational)
-
Controls the opening and shutting of the Chook House Door.
-
The Garden Steps System
(operational)
-
Controls the switching of the garden steps lights.
-
The Garden Watering System
(operational)
-
Controls the watering of various garden irrigation outlets.
-
The Relay Control
System (operational)
-
Uses an BeagleBone driving 8 relays to switch
the various circuits.
-
The Heating System (to
be reinstated)
-
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 (to be
reinstated)
-
Provides monitoring of the water storage facilities.
- The Solar System
(to be reinstated)
-
Provides monitoring of the solar photovoltaic systems,
together with the inverter and UPS sub-systems.
-
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.
1.2 TODOs
-
Need interlocking so that you cannot turn on chook up when
door going down and vice versa.
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 Progress since 2015
In Oct 2016 we moved house, since we had embarked upon a
complete renovation of the existing house, and we needed to
vacate the premises. All the house automation system had to
be dismantled, since every room in the house was undergoing
some sort of renovation, and the cellar was no exception. For
the remainder of 2016, all of 2017, and the first few months
of 2018, the HouseMade system was out of action.
Progress was slow in reinstating the system, partly because of
the disruption to the cellar, and partly because most of the
components were in storage. The cellar had been dug out to
expand the space available, and most of our spare time in 2019
was spent in making it more habitable. A new floor went in, a
baseboard for the new model railway constructed (see History), drawers
and shelves installed and a new (secondhand) rack to house the
house server and central replacement brought in. It made a
big difference to the space! This was largely complete by mid
2020, by which time the chook door opening mechanism had been
reinstated, and recabled.
A complete redesign of the system was undertaken, reusing some
components from the previous design, but now based upon an
event-driven server-based model. This is described below in
the relevant sections. The new hardware consists of a
BeagleBone Black (the relay driving processor), which talks to
a decentralized Relay Server controlling the relay driver, and
a decentralized Event Server, providing generalized event
services to web pages and the like. These servers have been
designed and tested on my desktop, but are slowly migrating to
the house server (a large rack-based disk farm built from
a variety of components). In addition, a Raspberry Pi Model 4
is being developed to handle data collection and logging.
As of Dec 2020, the new system is operational, driving the
chook door, garden lights (a new addition to the controlled
enviroment), and the garden irrigation system. Further
development to reinstate the heating system and data logging
systems is underway.
1.5 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 me 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).
(v2.0.0) The new version of CENTRAL is called
kerang, and is a BeagleBone running Debian 7.4
(wheezy). This has the unfortunate consequence that it is an
old version (and attempts to upgrade it have consistently
failed), and has no support for Arduinos, nor an operational
Python3. This shortcoming will no doubt have to be addressed
in future.
"HouseDefinitions.py" 2.2 =import xmlrpc.client
import datetime
CENTRAL="terang"
# system-wide definition of the house-controlling relay complement
RelayNames=[
'ChookUp', # 0 - the order of these is important
'ChookDown', # 1
'SouthVegBed', # 2
'GardenSteps', # 3
'Spare4', # 4 - unused from here,
'MiddleVegBed', # 5 - and unimplemented from here.
'TopVegBed', # 6
'CarportVegBed', # 7
'RainForest', # 8
'Woo2Plas', # 9
'FloodNDrain', # 10
'Heating' # 11
]
latitude = -37.8731753 # for 5 Fran Ct, Glen Waverley
longitude = 145.1643351
NEFname="/home/ajh/Computers/House/events.txt"
ThermostatSetting=19
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
NumberOfRelays = 5 # len(RelayNames) # changed in v2.0.0
RelayTable={}
for i in range(NumberOfRelays):
RelayTable[RelayNames[i]]=i
<HouseDefinitions: server connections and interfaces 2.3>
<HouseDefinitions: general routines 2.4>
2.2.1 HouseDefinitions: server connections and interfaces
<HouseDefinitions: server connections and interfaces 2.3> =BeagleServerAdr=('10.0.0.20',9999)
NTempBlocks=7 # max number of distinct temperature blocks allowed
HServer='http://%s:5000/heating' % (CENTRAL) # HeatingServer
MServer='http://%s/~ajh/cgi-bin/house.py' % (CENTRAL) # Main (web) server
RServer='http://%s:8001' % (CENTRAL) # RelayServer
EServer='
<EventServerRPCaddress 8.2>' # EventServer
SServer='http://%s:5000/solar' % (CENTRAL) # SolarServer
TServer='http://%s:5000/tank' % (CENTRAL) # TankServer
WServer='http://%s:5000/weather' % (CENTRAL) # WeatherServer
RelayServerGood=True
RelayServer=xmlrpc.client.ServerProxy(RServer)
# check that the server is running by testing one of its interfaces
try:
RelayServer.getState()
except:
# bad response, let users know
RelayServerGood=False
EventServerGood=True
EventServer=xmlrpc.client.ServerProxy(EServer)
# check that the server is running by testing one of its interfaces
try:
dummy=EventServer.printEvents()
except:
# bad response, let users know
EventServerGood=False
These are all the definitions required to talk to the
various pieces of code around the place. Most of them are
not yet implemented.
2.2.2 HouseDefinitions: general routines
<HouseDefinitions: general routines 2.4> =logging=True
def logMsg(msg,NewLine=True):
now=datetime.datetime.now()
if NewLine: msg+='\n'
if logging:
logfile=open('/home/ajh/logs/terang/house.log','a')
logfile.write("{}: {}".format(now.strftime("%H:%M:%S"),msg))
logfile.close()
else:
print(msg, end=' ')
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 BeagleBone System
There are four software components to this system:
- The GPIO-Relay Device Tree Specification
-
Defines the BeagleBone GPIO pins configurations. See
Derek Molloy's excellent tutorial page
for more information.
- BeagleDriver
-
The bottom layer software to hardware interface.
- BeagleServer
-
An interface to the outside world, providing primitive calls
to control the attached relays.
- BeagleClient
-
A simple program to test the server interface.
3.1 The GPIO setup
"AJH-GPIO-Relay.dts" 3.1 =/*
* Copyright (C) 2012 Texas Instruments Incorporated - http://www.ti.com/
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Purpose License Version 2 as
* published by the Free Software Foundation
*
* Original from: github.com/jadonk/validation-scripts/blob/master/test-capemgr/
*
* Modified by Derek Molloy for the example on www.derekmolloy.ie
* that maps GPIO pins for the example
*
* Modified by ajh (John Hurst) for use as 8-channel Relay driver
* and 2-channel input collector
*
*/
/dts-v1/;
/plugin/;
/{
compatible = "ti,beaglebone", "ti,beaglebone-black";
part-number = "AJH-GPIO-Relay";
version = "00A0";
fragment@0 {
target = <&am33xx_pinmux>;
__overlay__ {
pinctrl_test: AJH_GPIO_Relay_Pins {
pinctrl-single,pins = <
0x070 0x07 /* P9_11 30 OUTPUT MODE7 - Relay 1 Output - BN brown */
0x078 0x07 /* P9_12 60 OUTPUT MODE7 - Relay 2 Output - RD red */
0x074 0x07 /* P9_13 31 OUTPUT MODE7 - Relay 3 Output - OG orange */
0x048 0x07 /* P9_14 50 OUTPUT MODE7 - Relay 4 Output - YE yellow */
0x040 0x07 /* P9_15 48 OUTPUT MODE7 - Relay 5 Output - GN green (NOT used at present) */
0x04c 0x07 /* P9_16 51 OUTPUT MODE7 - Relay 6 Output - BU blue */
0x15c 0x07 /* P9_17 5 OUTPUT MODE7 - Relay 7 Output - VT violet (NOT used at present) */
0x158 0x07 /* P9_18 4 OUTPUT MODE7 - Relay 8 Output - GY grey (NOT used at present) */
0x03c 0x27 /* P8_15 47 INPUT MODE7 - pulldown proof open */
0x038 0x27 /* P8_16 46 INPUT MODE7 - pulldown proof closed */
/* Molloy originals
0x070 0x07 / * P9_11 30 OUTPUT MODE7 - Relay 1 Output * /
0x078 0x07 / * P9_12 60 OUTPUT MODE7 - The LED Output * /
0x184 0x2f / * P9_24 15 INPUT MODE7 none - The Button Input * /
0x034 0x37 / * P8_11 45 INPUT MODE7 pullup - Yellow Wire * /
0x030 0x27 / * P8_12 44 INPUT MODE7 pulldown - Green Wire * /
0x024 0x2f / * P8_13 23 INPUT MODE7 none - White Wire * /
*/
/* OUTPUT GPIO(mode7) 0x07 pulldown, 0x17 pullup, 0x?f no pullup/down */
/* INPUT GPIO(mode7) 0x27 pulldown, 0x37 pullup, 0x?f no pullup/down */
>;
};
};
};
fragment@1 {
target = <&ocp>;
__overlay__ {
test_helper: helper {
compatible = "bone-pinmux-helper";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_test>;
status = "okay";
};
};
};
};
See
Derek Molloy's excellent tutorial page
for more information. I just followed his example to
set things up. The steps were:
- Create and edit this dts file
-
Compile it using the dtc compiler to generate a dtbo file:
dtc -O dtb -o AJH-GPIO-Relay-00A0.dtbo -b 0 -@ AJH-GPIO-Relay.dts
(The build script in boneDeviceTree/overlay does this.)
- echo AJH-GPIO-Relay >$SLOTS
-
Copy the dtbo file into /lib/firmware on the
BeagleBone.
-
In the /sys/class/gpio directory, create links
to the gpio pins. For example, for GPIO23 (P8_13), an input:
echo 23 > export # create the link
cd gpio23 # check it now exists
cat direction # it should be 'in'
cat value # should change from 0 to 1 when input goes high
-
3.2 Restarting the BeagleBone Server
There are two issues:
- Setting up the device from scratch (e.g., system install), and
- After a reboot
The first is covered above in section GPIO Setup. What follows is what is
needed after a reboot or power shutdown.
3.2.1 Restore the Pin Definitions
The Beaglebone is rather special in that it allows software
reconfiguration of its I/O pins. For our purposes, we need to
follow through on the configurations defined in section GPIO Setup, and setup the user
interfaces to these hardware pins. Most of this is not
necessary if the relevant programs run as root, but we change
the permissions here so that users can also interact with the
hardware.
The settings here are lost on a reboot, and so it is necessary
to run this script after each reboot. Note that this script
must be run as root.
"exportBeaglePins.sh" 3.2 =#!/bin/bash
GPIO=/sys/class/gpio
# output pins
for i in 30 60 31 50 51 ; do
echo $i >$GPIO/export
done
# input pins
for i in 46 47 ; do
echo $i >$GPIO/export
done
# make the outputs
for i in 30 60 31 50 51 ; do
echo "out" >$GPIO/gpio$i/direction
echo 1 >$GPIO/gpio$i/value
done
# make the inputs
for i in 46 47 ; do
echo "in" >$GPIO/gpio$i/direction
done
# make user accessible
chmod g+w /sys
chmod g+w /sys/class
chmod g+w /sys/class/gpio
chgrp admin /sys
chgrp admin /sys/class
chgrp admin /sys/class/gpio
for i in 30 60 31 50 51 46 47 ; do chgrp admin $GPIO/gpio$i ; done
for i in 30 60 31 50 51 46 47 ; do chmod g+w $GPIO/gpio$i ; done
for i in 30 60 31 50 51 46 47 ; do chmod g+w $GPIO/gpio$i/value ; done
for i in 30 60 31 50 51 46 47 ; do chgrp admin $GPIO/gpio$i/value ; done
The key thing to note is the list of pin numbers 30 60 31
50 51, which define output pins. These numbers are used
to set gpio definitions, and then to set directions
(outputs) and initialize these outputs to 1 (which is
the "off" state for the driven relays).
Next, we define the input pins 46 47.
Finally, we set various permissions to make sure that users
(and not just root) can use these definitions.
3.2.2 Restart the BeagleBone Server proper
Once the pin definitions are restored, the BeagleBone server
can be restarted. This is done with a simple
$ ~/Computers/House/BeagleServer.py &
See below for the code for the BeagleServer.
3.3 the BeagleDriver.py program
"BeagleDriver.py" 3.3 =class relayDriver():
def __init__(self,virile):
f0=open("/sys/class/gpio/gpio30/value",'w')
f1=open("/sys/class/gpio/gpio60/value",'w')
f2=open("/sys/class/gpio/gpio31/value",'w')
f3=open("/sys/class/gpio/gpio50/value",'w')
f4=open("/sys/class/gpio/gpio51/value",'w')
self.valueFiles=[f0,f1,f2,f3,f4]
self.values=[False for i in range(5)]
self.reads=[0 for i in range(2)]
self.virile=virile
def switch(self,relay,value):
if value: v="0"
else: v="1"
if self.virile:
self.valueFiles[relay].write(v)
self.valueFiles[relay].flush()
self.values[relay]=value
def read(self):
res=''
r0=open("/sys/class/gpio/gpio47/value",'r')
r1=open("/sys/class/gpio/gpio46/value",'r')
readfiles=[r0,r1]
for i in range(2):
self.reads[i]=readfiles[i].read().strip()
res=res+"{}".format(self.reads[i])
r0.close();r1.close()
return res
def makeVirile(self):
self.virile=True
def makeSterile(self):
self.virile=False
def __str__(self):
str=""
for i in range(5):
if self.values[i]:
str+="o"
else:
str+="."
return str
This code provides a class that drives the relays directly
through the GPIO pins of the BeagleBone. The init method of
the class creates a file for each GPIO pin in use. This file
is available for writing, and by writing the appropriate value, 1
for on or 0 for off, to the file turns the nominated relay
attached to the corresponding GPIO pin (labelled gpiopin
number) on or off.
The second method, switch, turns a single bit/relay on
or off. The input request is a pair of digits, the first of
which defines the relay number to be used (0-origin), and the
second digit of which defines the on (1) or off (0) desired
state of this relay. Note that the file has to be flushed
after writing, for the new data value to be transferred
immediately. We preserve the new state in the class entity
values.
The third and fourth methods control the activity of the
driver. It can be switched into an ineffective mode, called
sterile, by invoking the method makeSterile, and
conversely, made active by invoking the method
makeVirile. The flag entity virile controls
this activity, and simply enables/disables the file writes in
the switch method.
The fifth and last method renders the current saved state into
a character string, which is used to return the new state to
the calling environment. An 'o' in this string indicates that
the corresponding relay is enabled, and inactive relays are
shown as '.'.
3.4 The BeagleServer.py program
"BeagleServer.py" 3.4 =#!/usr/bin/python
# no python3 on this machine
import datetime
import SocketServer
import re
import BeagleDriver
import sys
BeagleServerAdr=('10.0.0.20',9999)
driver=BeagleDriver.relayDriver(False) # initially sterile
class MyRelayServer(SocketServer.BaseRequestHandler):
'''
The request handler class for our server.
It is instantiated once per connection to the server, and must
override the handle() method to implement communication to the
client.
'''
def handle(self):
# self.request is the TCP socket connected to the client
line = self.request.recv(1024).strip()
#print "{} wrote:".format(self.client_address[0])
#print "current: {}, request: {}".format(driver,line)
if line:
res=re.match('(\d) *(\d)',line)
if res:
relay=int(res.group(1))
value=int(res.group(2))
driver.switch(relay,value)
else:
if line=='virile':
driver.makeVirile()
print("driver is now active!")
elif line=='sterile':
driver.makeSterile()
print("driver disabled")
elif line=="reset":
print(driver)
for i in range(5):
driver.switch(i,False)
elif line=='read':
res=driver.read()
#if res=='11':
# if laststate=='open': res='11'
# elif laststate=='closed': res='00'
# don't return normal state
#print(res)
self.request.sendall(res)
return
else:
print("did not recognize request:>{}<".format(line))
# just print and send back the new driver state
self.request.sendall("{}".format(driver))
if __name__ == "__main__":
# Create the server, binding to localhost on port 9999
server = SocketServer.TCPServer(BeagleServerAdr, MyRelayServer)
# Activate the server; this will keep running until you
# interrupt the program with Ctrl-C
now=datetime.datetime.now()
print("{} BeagleServer starts".format(now))
laststate=''
server.serve_forever()
This code runs a server to interface with the relay driver
code (BeagleDriver.py). It imports the driver class,
and creates a TCP socket server that reads and passes a relay
state string directly through to the driver. This is done for
reasons of simplicity and reliability. The handle
method returns the new state as a string representation.
There are three additional imputs that are recognized:
reset, sterile, and virile. The first
returns all relay states to off, and the second and third
control the driver activity, as described above in <BeagleDriver.py 3.3>
3.5 The BeagleClient.py program
"BeagleClient.py" 3.5 =#!/home/ajh/binln/python
import socket
import sys
BeagleServerAdr=('10.0.0.20',9999)
data = " ".join(sys.argv[1:])
# Create a socket (SOCK_STREAM means a TCP socket)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# Connect to server and send data
sock.connect(BeagleServerAdr)
sock.sendall(data + "\n")
# Receive data from the server and shut down
received = sock.recv(1024)
finally:
sock.close()
print "Sent: {}".format(data)
print "Received: {}".format(received)
A simple little program to test the operation of the Beagle
relay software. It reads a pair of digits from the invoking
CLI line, and passes them through to the relay driver via the
server.
4. The Relay Server
Currently a Beagle Bone Black (see previous section) is used to
interface to the house relays, 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:
4.1 Relay Server Code
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" 4.1 =#!/home/ajh/binln/python3
import datetime
import os
import re
# import solar # removed in v2.0.0
import socket
import sys
import subprocess
import threading
import time
import ChookDoor
# import usbFind # removed in v2.0.0
from xmlrpc.server import SimpleXMLRPCServer
from xmlrpc.server import SimpleXMLRPCRequestHandler
from HouseDefinitions import NumberOfRelays,RelayNames,CENTRAL,BeagleServerAdr,logMsg
# Restrict to a particular path.
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/RPC2',)
# Create server
server = SimpleXMLRPCServer(('0.0.0.0', 8001),
requestHandler=RequestHandler,
logRequests=False)
server.register_introspection_functions()
print("RelayServer registers RPC")
# open the logfile
logname="/home/ajh/logs/terang/RelayServer.log"
logs=open(logname,'a')
<RelayServer: connect to the BeagleServer 4.2>
# define the relay state
try:
state=serverSend('')
except:
print("Cannot talk to the Beagle Server - have you started it?")
sys.exit(1)
currentState=[0 for i in range(NumberOfRelays)]
for i in range(NumberOfRelays):
if state[i]=='o': currentState[i]=1
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
<relayserver: strState 4.3>
<relayserver: define the RPC-Server interface 4.4>
# Define and Register the readDoor function
<relayserver: readDoor 4.6>
# Define and Register the getState function
<relayserver: getState 4.5>
# Define and Register the setState function
<relayserver: setState 4.7>
# Define and Register the setBit function
<relayserver: setBit 4.8>
# Define and Register the setBitOn function
<relayserver: setBitOn 4.9>
# Define and Register the setBitOff function
<relayserver: setBitOff 4.10>
# Define and Register the getTank function
<relayserver: define getTank 4.11>
# Define and Register the getTimer function
<relayserver: getTimer 4.12>
# Define and Register the start function
<relayserver: start 4.14>
# define the count down timers process
<relayserver: countDown 4.15>
# Define and Register the getSolar function
<relayserver: getSolar 4.13>
# Run the server's main loop
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
logs.write("%s: RelayServer restarts on device %s\n" % (now,'relayDevice'))
logs.flush(); os.fsync(logs.fileno())
# counters commented out v2.0.0
counters=countDown()
counters.start()
print("RelayServer starts serving")
server.serve_forever()
counters.join()
logs.close()
4.1.1 RelayServer: connect to the BeagleServer
<RelayServer: connect to the BeagleServer 4.2> =def serverSend(data):
# Create a socket (SOCK_STREAM means a TCP socket)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# Connect to server and send data
sock.connect(BeagleServerAdr)
sock.sendall("{}\n".format(data).encode())
# Receive data from the server and shut down
received = sock.recv(1024).decode()
finally:
#sock.shutdown(socket.SHUT_RDWR)
sock.close()
return received
This routine is a more basic BeagleBone server connection
than the RelayChannel routine, described below. It
does nothing more than send and received raw data from the
BeagleBone server, which is responsible for actually
energising the various relays. Only one item of data is
received and returned for each invocation of the routine,
and the received parameter can be
- ''
-
The empty string, which will retrieve the current status
of the relays in the form of a string of '.'s or 'o's,
with the former indicating the corresponding relay is
not energised, and the latter indicating that it is.
The relays are numbered zero origin from the left of the
string.
- dd
-
A pair of digits, the first of which is a relay number,
and the second of which is a 1 or 0, indicating the the
corresponding relay is to be turn on or off
respectively.
- read
-
The word 'read', which reads the current state of the
chook door as a pair of diigits, the first/leftmost for
the up proving circuit, the second of which is for the
down proving circuit. '0' indicates that the circuit is
proved, '1' indicates that it is not.
4.1.2 Define the convert State to String function
<relayserver: strState 4.3> =# 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
This short routine converts the internal representation of
the current state of the relays into a string form
suitable for printing.
4.1.3 RelayServer: define the RPC-Server interface
<relayserver: define the RPC-Server interface 4.4> =# define the relay control server interface
def relayChannel(data):
now=datetime.datetime.now()
nowTime=now.strftime("%Y%m%d:%H%M")
logMsg("relayChannel gets {}".format(data))
for i in range(len(data)):
msg="{}{}\n".format(i,data[i])
logMsg("relayChannel sending {}".format(msg),NewLine=False)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Connect to server and send data
sock.connect(BeagleServerAdr)
sock.sendall(msg.encode())
# Receive data from the server and shut down
received = sock.recv(1024)
finally:
sock.close()
logMsg("relayChannel returns {}".format(received))
return received
This chunk defines how the Relay Server (an RPC interface
server) talks to the low level server (a raw HTTP server).
The Relay Server provides high level operations, that allow
individual relays to be turned on and off, while the
low-level server has an interface that is just a string of
"bits", indicating the new desired state of the relays.
This string contains '.'s and 'o's, indicating 'off' and
'on' relays, numbered from 0 from the left, up to the total
NumberOfRelays (minus one, because of zero-origin indexing).
This allows other programs to talk more directly to the
low-level relay interface, without needing the complexity of
the RPC interfaces.
These RPC interfaces are:
- readDoor()
-
Reads the state of the chicken door.
- getState()
-
Returns an array of bits (0,1) indicating the current
state of the relays, as defined by the low-level
interface (and thus allowing for asynchronous
interactions with the relays).
- setState(newState)
-
Resets the relays according to the bits in the array
newState, where a '0' indicates the relay is
(now) to be turned off, and a '1' indicates the relay
is (now) to be turned on.
- setBit(relay,newState)
-
Set the relay identified by relay (numbered 0
and up) to the new state newState.
- setBitOn(relay)
-
Set the relay identified by relay (numbered 0
and up) to 'On'.
- setBitOff(relay)
-
Set the relay identified by relay (numbered 0
and up) to 'Off'.
4.1.4 relayserver: getState
<relayserver: getState 4.5> =def getState():
state=serverSend('')
currentState=[0 for i in range(NumberOfRelays)]
for i in range(NumberOfRelays):
if state[i]=='o': currentState[i]=1
return currentState
server.register_function(getState, 'getState')
4.1.5 relayserver: readDoor
<relayserver: readDoor 4.6> =def readDoor():
state=serverSend('read')
return state
server.register_function(readDoor, 'readDoor')
4.1.6 relayserver: setState
<relayserver: setState 4.7> =def setState(newState):
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
nrels=len(newState)
for i in range(nrels):
currentState[i]=newState[i]
s=strState(currentState)
relayChannel(s)
logs.write("%s setState(%s)\n" % (now,s))
logs.flush(); os.fsync(logs.fileno())
return (currentState,"OK")
server.register_function(setState, 'setState')
4.1.7 relayserver: setBit
<relayserver: setBit 4.8> =def setBit(bitNo,newValue):
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
currentState=getState()
print("{} setBit({},{}) starts with {}".format(now,bitNo,newValue,currentState))
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
setState(currentState)
s=strState(currentState)
#relayChannel(s)
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.
4.1.8 relayserver: setBitOn
<relayserver: setBitOn 4.9> =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.
4.1.9 relayserver: setBitOff
<relayserver: setBitOff 4.10> =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.
4.1.10 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 4.11> =# 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/terang/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
server.register_function(getTank, 'getTank')
4.1.11 relayserver: getTimer
<relayserver: getTimer 4.12> =def getTimer(bitNo):
remTime=currentTime[bitNo]
return remTime
server.register_function(getTimer, 'getTimer')
4.1.12 relayserver: getSolar
<relayserver: getSolar 4.13> =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')
4.1.13 relayserver: start
<relayserver: start 4.14> =def start(bitNo,timeon):
now=datetime.datetime.now().strftime("%Y%m%d:%H%M%S")
currentState=getState()
logs.write("%s: RelayServer.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)
setState(currentState)
# turning the bit off is taken care of by the countDown process
return (currentState,"OK")
server.register_function(start, 'start')
4.1.14 relayserver: countDown
<relayserver: countDown 4.15> =class countDown(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
while True:
if nonZeroTimes:
currentState=getState()
for bitNo in nonZeroTimes:
currentTime[bitNo]-=1
if currentTime[bitNo]==0:
# turn this bit off and log the fact
currentState[bitNo]=0
s=setState(currentState)
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.
4.2 Starting the Relay Server
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.
It is invoked by the make script as make start-relay.
"startRelayServer.sh" 4.16 = **** File not generated!LOGDIR='/home/ajh/logs/terang'
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
# use logging version for now
/usr/bin/python ${HOUSE}/RelayServer.py >${LOGDIR}/RelayServer.log 2>&1 &
#/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
4.3 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.
This program is used only for testing the Relay Server, and is
not used by the HouseMade system per se.
"RelayControl.py" 4.17 =#!/home/ajh/binln/python3
import time
import os
import sys
import xmlrpc.client
from HouseDefinitions import *
def main(device,timeRunning):
# get relay bit for device
try:
bitNo=RelayTable[device]
except KeyError:
print("bad relay function key %s" % (device))
sys.exit(1)
# start the relay for timeRunning seconds
(state,ok)=RelayServer.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)
5. The Events Module
This module contains definitions of the Event and
EventList classes, suitable for importing into other
modules. At the moment, there is no standalone (main)
function.
"Events.py" 5.1 =
6. The Event Class
A significant shift in the design (from HouseMade v2.2.0) is the
introduction of the Event class, to encapsulate data pertaining
to an event. An event can now occur at any time in the future,
and is day of the week aware, as well as time of the year. This
has implications for seasonal variations, such as sunrise/sunset
times, and for summer/winter changes.
<Event class: definition 6.1> =class Event():
def __init__(self,dictn=None,time=None,weekday=None,day=None,month=None,\
device=None,operation=None):
self.time=time
self.weekday=weekday
self.day=day
self.month=month
self.device=device
self.operation=operation
if dictn:
for k in dictn.keys():
setattr(self,k,dictn[k])
pass
def dictRep(self,ev):
for key in ev.keys():
setattr(self,key,ev[key])
return
def __str__(self):
rtn='{'
rtn+='month={}, '.format(self.month)
rtn+='day={}, '.format(self.day)
rtn+='time={}, '.format(self.time)
rtn+='device={}, '.format(self.device)
rtn+='operation={}'.format(self.operation)
rtn+='}'
return rtn
<Event class: compare two events 6.2>
The Event class provides data that defines an event in
the system. There are a number of attributes, defined as
follows:
- month
-
The month of the year, and integer in the range 1-12,
representing the months January-December. A value of none
indicates the event occurs every month of the year.
- day
-
The day of the month on which the event occurs, an integer
in the range 1-31. A value of None indicates that the event
occurs every day of the month.
- weekday
-
A value in the range 0-6, where 0 represents Sunday, and 6
represents Saturday. This is for events that only occur on
specific days of the week. A value of None indicates that
the event occurs on every day of the week.
- time
-
A value in the form HH:MM, representing time in a 24 hour
format, and defining the time of day at which an event
occurs.
- device
-
The name of the device for which the event occurs. This is
a text field, and currently has the values ChookDoor,
SouthVegBed, and GardenLights.
- operation
-
The parameter for the event. This is also a text field, and
currently has the values on, off, up,
down, or a string of digits, representing an integer
value.
Note that an event can be created with zero or more initial
values for these attributes through the use of optional
parameters. The optional parameter dictn can be used
with a dictionary of values (with keys as for the fields defied
above) to initialize the event. These values override any
defined with the other values.
<Event class: compare two events 6.2> =def compare(self,a):
if a.time > self.time: return -1
elif a.time < self.time: return 1
else: return 0
def __gt__(self,other):
return self.compare(other)
def __lt__(self,other):
return -self.compare(other)
def __eq__(self,other):
return self.time==other.time
Compare this event against another. For comparison purposes,
only the times are relevant. If this event occurs first,
return -1. If it occurs at the same time as event a, return 0.
Otherwise, return 1.
7. The EventList class
<EventList class: definition 7.1> =
The EventList maintains a list of events, sorted
chronologically. Past events are possible, and are ignored for
scheduling purposes.
<EventList class: add event 7.2> =def add(self,e,dupl=False):
# e is an Event, dupl is boolean, True=>duplicates allowed
#print("adding {}, is type {}, duplicate={}".format(e,type(e),dupl))
if not self.list: self.list=[e]; return
for l in self.list:
#print ("check event {}, type is {}".format(l,type(l)))
same=True # until proven false
for attr in ['month','day','time','device','operation']:
if getattr(l,attr)!=getattr(e,attr): same=False ; break
#print("Adding Event, same={}, duplicate={}".format(same,dupl))
if same and not dupl:
print("Event {} not added - duplicate event".format(e))
return
if e.compare(l) < 0:
i=self.list.index(l)
self.list.insert(i,e)
return
self.list.append(e)
When adding an event, it is placed into the list according to
its time attribute, so that the preceding event has a
time value that is the same (see <Event class: compare two events >, and the following event has a time
that is the same or later.
The dupl parameter, when True, allows duplicate events to
be added. Normally, duplicate events cannot be added when this
parameter is False
<EventList class: delete event 7.3> =def delete(self,a): # a is an Event
self.list.remove(a)
return
Remove a given event from the list.
<EventList class: sort events 7.4> =def sort(self):
old=self.list
self.list=[]
for e in old:
self.add(e)
return
(Re)sort the event list. This is done with an insertion sort,
to ensure that the list is recreated in the same order as when
events are added one by one.
<EventList class: nextEvent 7.5> =def nextEvent(self,now):
# return index of first event with time =/> now
#print(self.list)
i=0
for e in self.list:
#print(e)
if now<=e.time:
return i
i+=1
return None
Return the first event in the list with time equal or later to
the given time stamp now. Return None if there
are no such events. (This is a change in the definition -
previously the last event would be returned in this case, which
violates the postcondition.)
<EventList class: load events 7.6> =def load(self):
f=open(EVENTFILE,'r')
for l in f.readlines():
res=re.match('{month=(.*), day=(.*), time=(.*), device=(.*), operation=(.*)}',l)
if res:
ev=Event(month=res.group(1), day=res.group(2), \
time=res.group(3), device=res.group(4),\
operation=res.group(5))
self.add(ev)
else:
print("Cannot parse {}".format(l))
f.close()
return
Load events from a file. Existing events are retained, and new
events are inserted to preserve the chronological ordering.
<EventList class: save events 7.7> =def save(self):
f=open(EVENTFILE,'w')
content=self.__str__()
f.write(content)
f.close()
return
Save all current events to a file, which may be later reloaded
with load. Be aware that on reloading, any existing
events with the same parameters will become duplicate events.
8. The Event Server
<EventServerRPCport 8.1> = 8002
<EventServerRPCaddress 8.2> = http://terang:<EventServerRPCport 8.1>
Define the address of the EventServer port.
"EventServer.py" 8.3 =#!/home/ajh/binln/python3
import datetime
import sys
import time
from Events import Event,EventList
class ShutDown(Exception):
pass
from multiprocessing import Process
from xmlrpc.server import SimpleXMLRPCServer
from xmlrpc.server import SimpleXMLRPCRequestHandler
# data structures
el=EventList()
# Restrict to a particular path.
class RequestHandler(SimpleXMLRPCRequestHandler):
rpc_paths = ('/RPC2',)
# Create server
port=('0.0.0.0',
<EventServerRPCport 8.1>)
server = SimpleXMLRPCServer(port,
requestHandler=RequestHandler,
allow_none=True)
server.register_introspection_functions()
#print("EventServer registers RPC")
# identify the next event. None means not yet identified.
# Caveat! nextEventPointer can take the value 0, meaning the first
# event. Be careful to distinguish 0 from None, they both test False!
nextEventPointer=None
<Event Server: log message handling 8.4>
<Event Server: calling points 8.5>
<Event Server: serverprocess routine 8.6>
<Event Server: main routine 8.7>
if __name__ == '__main__':
main()
The EventServer is a development of the
EventManager as a means of providing a generic event
service.
8.1 Event Server Log Message Handling
<Event Server: log message handling 8.4> =# open the logfile
logname="/home/ajh/logs/terang/EventServer.log"
logs=open(logname,'a')
logs.write("\n") # space from any previous log messages
def logmsg(msg):
now=datetime.datetime.now()
nowstr=now.strftime("%Y%m%d:%H%M%S")
logs.write("{}: {}\n".format(nowstr,msg))
logs.flush()
8.2 Event Server calling points
<Event Server: calling points 8.5> =def add(evd,dupl,handle=None):
# evd comes in as a dictionary, must convert it ...
ev=Event() # first make empty event
for key in evd.keys():
setattr(ev,key,evd[key])
el.add(ev,dupl=dupl)
# note that add always adds in order
logmsg("server adds event {}".format(ev))
return
server.register_function(add, 'add')
def remove(evn):
if type(evn) is int:
# delete event number evn
if evn<len(el.list):
del el.list[evn]
return
elif type(evn) is Event:
# find event that matches evn in device and operation
pass # needs work!
server.register_function(remove, 'remove')
def setNext(curtime):
'''Use the current time to identify the next event to be scheduled,
and then set nextEventPointer to point to this event. nextEventPointer is
advanced by getNext.'''
global nextEventPointer
if not curtime:
now=datetime.datetime.now()
curtime=now.strftime("%H:%M")
nextEventPointer=el.nextEvent(curtime)
server.register_function(setNext, 'setNext')
def advanceNext():
'''move to the next event in the list'''
global nextEventPointer
if nextEventPointer is None:
nextEventPointer=0
elif nextEventPointer < len(el.list):
nextEventPointer+=1
return
server.register_function(advanceNext, 'advanceNext')
def showNext():
'''return the next event in the list. The nextEventPointer is not
advanced.'''
global nextEventPointer
if nextEventPointer is None:
return None
if nextEventPointer >= len(el.list):
return None
ev=el.list[nextEventPointer]
logmsg("show next event returns event number {}, which is {}".format(nextEventPointer,ev))
return ev
server.register_function(showNext, 'showNext')
def moreEvents():
global nextEventPointer
if nextEventPointer is not None and nextEventPointer < len(el.list):
return True
else:
return False
server.register_function(moreEvents, 'moreEvents')
def printEvents():
for ev in el.list:
print(ev)
return
server.register_function(printEvents, 'printEvents')
def getEvent(i):
if i>=len(el.list): return None
else: return el.list[i]
server.register_function(getEvent, 'getEvent')
def matchEvents(ev):
# return a list of events matching ev in device and operation
l=[]
for e in el.list:
if e.device==ev.device and e.operation==ev.operation:
l.append(e)
return l
server.register_function(matchEvents, 'matchEvents')
def sortEvents():
el.sort()
server.register_function(sortEvents, 'sortEvents')
def loadEvents():
el.load()
server.register_function(loadEvents, 'loadEvents')
def saveEvents():
el.save()
server.register_function(saveEvents, 'saveEvents')
def registerCallback(device,routine):
dispatcher[device]=routine
server.register_function(registerCallback, 'registerCallback')
These are the entry points for calls on the
EventServer. The logmsg are commented out until
I can work out how to output messages from a server subprocess
(it involves using a Queue or a Pipe, see The
Python3 Library).
8.3 Event Server: serverprocess routine
<Event Server: serverprocess routine 8.6> =def serverprocess(logmsg):
try:
server.serve_forever()
except KeyboardInterrupt:
pass
The serverprocess routine is the heart of the event
server. Its role is to package up the separate process of
running the server, while allowing the event manager to manage
its events. Not sure about the KeyboardInterrupt, though.
8.4 Event Server: main routine
<Event Server: main routine 8.7> =def main(testing=True,forReal=True):
logmsg("EventServer starts serving on port {}".format(port))
serverp = Process(target=serverprocess, args=(logmsg,))
serverp.start()
try:
serverp.join()
except KeyboardInterrupt:
logmsg("Keyboard Interrupt received! ")
now=datetime.datetime.now()
nowstr=now.strftime("%Y%m%d:%H%M%S")
logmsg("EventServer stops serving (main)")
logs.close()
9. The Event Editor
This is a new component to the HouseMade system. It provides a
web interface to the EventServer, allowing the user to
view and edit the current list of events.
"eventEditor.py" 9.1 =
Define global stuff, then the various routines to perform the
EventEditor operations.
"eventEditor.py" 9.2 =try:
el=getAllEvents()
except ConnectionRefusedError:
print('<p style="background-color:red; font-style:italic;color:white">',end='')
print('Connection to EventServer refused - is it running?</p>')
sys.exit(0)
#print(el)
if 'action' in form:
request=form['action'].value
else:
request='home'
entry=None
if 'entry' in form:
entry=int(form['entry'].value)
month=day=time=device=operation=''
if 'month' in form: month=form['month'].value
if 'day' in form: day=form['day'].value
if 'time' in form:
time=form['time'].value
if not ':' in time:
time="{:s}:{:s}".format(time[0:2],time[2:4])
if 'device' in form: device=form['device'].value
if 'operation' in form: operation=form['operation'].value
#print("<p>Request is for {}, entry={}</p>".format(request,entry))
Now collect the events from the server, and the edit parameters
from the URL form.
"eventEditor.py" 9.3 =if request=='home':
page=makeHomePage()
elif request=='add':
# Need to know where to add this entry (given by value of entry)
# added after entry number 'entry' with time set to that of entry number entry
# adding to empty list is a special case
if len(el.list)==0:
# make first entry event
a=Event()
a.time='00:00'
else:
# get event at this entry number
e=es.getEvent(entry)
a=Event(dictn=e)
print("Adding event {}".format(a))
es.add(a,dupl=True)
#print(len(el.list))
page=makeHomePage()
elif request=='edit':
page=makeEditPage(entry,month=month,day=day,time=time,device=device,operation=operation)
elif request=='delete':
print("<p>deleting entry number {}</p>".format(entry))
es.remove(entry)
page=makeHomePage()
else:
page="<p>Action <i>{}</i> not implemented</p>".format(request)
print("{}".format(page))
Examine the request, and perform the appropriate
task. add adds a new event to the table, using the
current event as the default value. edit updates any
edited information, and delete deletes the given element.
9.1 The EventEditor Instructions
<EventEditor: print instructions 9.4> =print('''
<p>
Events are shown, one per line in the table. '*' for <b>Month</b>
and <b>Day</b> indicate every month/day is involved. (These can be
names from the lists Jan-Dec or Sun-Sat, but this is not implemented
as yet.)
</p>
<p>
<b>Times</b> are to be entered in 24 hour format, either as 'HH:MM'
or as 'HHMM' (but are stored and displayed in the first format).
The <b>Device</b> will be a pull-down from a menu, but is currently
implemented only as a text field. Similarly for the
<b>Operation</b> field.
</p>
<p>
The <b>Change</b> column buttons save/enter any changed data on the
given line. <b>Add</b> adds a new event, initialized to the data on
the given line. <b>Delete</b> will delete the event. Note that if
you need to add an event before the first entry, simply click on
<b>Add</b> for the first entry, and edit the time field to be sooner
than the given time.
</p>
<p>
The <b>NOW</b> line indicates the current time in the sequence of
events, and the next line will be the next event to be scheduled.
</p>
''')
9.2 EventEditor: get current events routine
<EventEditor: define get current events routine 9.5> =# get all the current events
def getAllEvents():
i=0
es.sortEvents()
x=es.getEvent(0)
el=EventList()
while x:
#print(x)
ev=Event(dictn=x)
el.add(ev)
i+=1
x=es.getEvent(i)
#el.sort()
return el
9.3 EventEditor: Make Home Page
<EventEditor: define make home page routine 9.6> =def makeHomePage():
es.sortEvents()
el=getAllEvents()
numEvents=len(el.list)
now=datetime.datetime.now()
nowTime=now.strftime("%H:%M")
#print("<p>at start of makeHomePage, el={}".format(el))
page='<p>Current events are:</p>\n'
page+=' <table style="margin-left:40pt;" border="4pt solid green">\n'
page+=' <tr><th>Month</th><th>Day</th><th>Time</th><th>Device</th><th>Operation</th>'
addstr='Add<br/>After'
if numEvents==0:
addstr='<form action="http://terang/~ajh/cgi-bin/eventEditor.py?action=add&entry=0" method="post">'
addstr+='<button type="submit" value="0">Add</button>'
addstr+='</form>'
page+=' <th>Save<br/>Changes</th><th>{}</th><th>Delete this entry</th>'.format(addstr)
page+=' </tr>\n'
i=0 ; doneNow=False
for ev in el.list:
line=' <tr>\n'
if ev.time and not doneNow and nowTime < ev.time:
line+='<td colspan="2" align="center" bgcolor="pink">NOW</td>'
line+='<td>{}</td>'.format(nowTime)
line+='<td colspan="5" align="center" bgcolor="pink">NOW</td></tr><tr>\n'
doneNow=True
month=ev.month
if not month: month='*'
line+=' <form action="http://terang/~ajh/cgi-bin/eventEditor.py?action=edit&entry={}" method="post">\n'.format(i)
line+=' <td><input type="text" name="month" value="{}"></input></td>\n'.format(month)
day=ev.day
if not day: day='*'
line+=' <td><input type="text" name="day" value="{}"></input></td>\n'.format(day)
time=ev.time
line+=' <td halign="center"><input align="center" type="text" name="time" value="{}"></input></td>\n'.format(time)
device=ev.device
#line+='<td><form>'
#line+=' <select name="device">'
#for dev in ['ChookDoor','SouthVegBed','GardenLights']:
# if dev==device: selected=' selected'
# else: selected=''
# line+=' <option value="{}{}">{}</option>'.format(dev,selected,dev)
#line+=' </form></select>'
#line+='</td>\n'
line+=' <td><input type="text" name="device" value="{}"></input></td>\n'.format(device)
operation=ev.operation
line+=' <td><input type="text" name="operation" value="{}"></input></td>\n'.format(operation)
# Enter column
line+=' <td>\n'
line+=' <button type="submit" value="{}">Enter</button></td>\n'.format(i)
line+=' </td>\n'
line+=' </form>\n'
# Add column
line+=' <form action="http://terang/~ajh/cgi-bin/eventEditor.py?action=add&entry={}" method="post">\n'.format(i)
line+=' <td align="center" valign="middle">\n'
line+=' <button type="submit" value="{}">Add</button>\n'.format(i)
line+=' </td>\n'
line+=' </form>\n'
# Delete column
line+=' <form action="http://terang/~ajh/cgi-bin/eventEditor.py?action=delete&entry={}" method="post">\n'.format(i)
line+=' <td style="align:center; valign:bottom">\n'
line+=' <button type="submit" value="{}">Delete</button>\n'.format(i)
line+=' </td>\n'
line+=' </form>\n'
line+=' </tr>\n'
page+="{}".format(line)
i+=1
if not doneNow:
page+='<td colspan="8" align="center" bgcolor="pink">NOW</td></tr><tr>\n'
page+=' </table>\n'
page+='</p>\n'
return page
9.4 EventEditor: Make Edit Page
<EventEditor: define make edit page routine 9.7> =def makeEditPage(entry,time=None,weekday=None,day=None,month=None,\
device=None,operation=None):
el=getAllEvents()
if entry>= len(el.list):
ev=Event()
else:
ev=el.list[entry]
es.remove(entry)
ev.month=month
ev.day=day
ev.time=time
ev.device=device
ev.operation=operation
es.add(ev)
return makeHomePage()
10. The Event Scheduler
This program takes the place of the old EventManager, and
uses the EventServer to maintain the collection of events
to be scheduled. Its basic algorithm is much the same: look at
the next event from the current time, wait until its time to be
scheduled has arrived, and then schedule it.
Some thought does need to be given to events that may be added
to the event list between now and the scheduling time (should
they be ignored, should they take precedence of the current next
event, etc.). A possible solution: if the next event's time is
now, then schedule it. Otherwise, sleep for a minute, and then
recheck the next event. It is possible that the wait time
should be less than a minute, to allow for processing time,
which could see the wait time to be longer than a minute,
thereby missing the scheduling time of an event.
Also, if two or more events are scheduled for the same time,
only one will be scheduled. It is indeterminate which of the
simultaneous events will be scheduled. This needs further
careful thought.
Some thoughts. Create a 'mark now' which identifies the initial
next event time. Set a pointer to mark the next event. Provide
an interface to inspect the next event (but keep the same
marker). Provide a separate 'move marker' to advance to the
next event. This should get around the duplicate schedule time
problem. But do need to check times of events, particularly
that we don't fall behind in processing duplicate events.
"EventScheduler.py" 10.1 =#!/home/ajh/binln/python3
import datetime
from Events import Event,EventList
import sys
import time
import xmlrpc.client
import GardenSteps
import GardenWater
import ChookDoor
print("EventScheduler starts")
# connect to the server
EServer='
<EventServerRPCaddress 8.2>'
es=xmlrpc.client.ServerProxy(EServer,allow_none=True)
# dispatcher is a dictionary of call-back routines indexed by device, and
# called when scheduled with the given parameter for the event.
dispatcher={}
gs=GardenSteps.GardenSteps()
dispatcher['GardenSteps']=gs.handleEvent
gw=GardenWater.GardenWater()
dispatcher['SouthVegBed']=gw.handleEvent
cd=ChookDoor.ChookDoor()
dispatcher['ChookDoor']=cd.handleEvent
es.loadEvents()
try:
lastTime=None
now=datetime.datetime.now()
nowTime=now.strftime("%H:%M")
es.setNext(nowTime)
while es.moreEvents():
next=es.showNext()
ev=Event(dictn=next)
print("at {}, next event is {}".format(now.strftime("%H%M%S"),ev))
while ev.time==nowTime:
print("Schedule event {}".format(ev))
dev=ev.device; op=ev.operation
if dev in dispatcher:
dispatcher[dev](dev + ' ' + op)
print("Event {}({}) dispatched".format(dev,op))
else:
print("No handler for event")
es.advanceNext()
next=es.showNext()
ev=Event(dictn=next)
now=datetime.datetime.now()
nowTime=now.strftime("%H:%M")
secs2zero=60-now.second
time.sleep(secs2zero)
es.setNext(nowTime)
pass
except KeyboardInterrupt:
print("EventScheduler terminated by KeyboardInterrupt")
es.saveEvents()
sys.exit(0)
print("EventScheduler runs out of events")
es.saveEvents()
11. The Chook Door
This section is being rebuilt, and obsoletes the old section of
the same name. The philosophy used is quite different, and is
based on an event handling model. This module is the first to
be rebuilt according to this model.
This code has been migrated from HouseMade and slightly modified
to now work with the EventServer model. The original has been
changed to ChookDoor to avoid confusion. Currently
(version 1.0.4) the interface does not handle sunrise and sunset
times.
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.
The EventScheduler module has the responsibility for driving
the chook door events. Each module requiring action at a
particular time needs to register with the event scheduler with a
request to be alerted when a particular time is reached. This
may be a time of day, or every minute, or every hour, or even a
day of the week.
This ChookDoormodule just acts as a passive interface,
providing the necessary code to actually perform the chook door
operation. The are two base level operations: openDoor
and closeDoor, with obvious meanings. They communicate
by means of a socket connection on port 9999 to the relay server
running on kerang, accessed through the generic interface
serverSend.
A third operation chookDoor(p) uses a parameter p
to indicate whether an open or close operation is required.
Either of 'open'/1 can be used to invoke the opening, or
'close'/'shut'/0 to close the door.
A fourth operation, doorState, can be used to interrogate
the current door status.
The routine handleEvent, is the generic interface to the
EventScheduler, and bundles all the driver routines.
Important Note: This interface may change later, when
the relay drivers are moved to the new house computer,
ouyen. Then the code will be moved to a generic Relay Driver.
"ChookDoor.py" 11.1 =import socket
import sys
import time
#import currentState
import datetime
#import eventManager
import getopt
import re
import sys
from HouseDefinitions import *
from suntime import Sun, SunTimeException
class BadChook(BaseException):
pass
compute=0
version='1.0.0'
latitude = -37.8732083333333 # for Glen Waverley
longitude = 145.164671666667
chookFileName='/home/ajh/Computers/House/suntimes.txt'
timezone=datetime.timezone(datetime.timedelta(hours=10))
now=datetime.datetime.now(timezone)
<ChookDoor: misc routines 11.2>
<ChookDoor: class ChookDoor 11.3>
<ChookDoor: main 11.15>
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()
11.1 ChookDoor: misc routines
<ChookDoor: misc routines 11.2> =def parse(pat,line):
res=re.match(pat,line)
if res:
return res.group(1)
else:
return ''
11.2 ChookDoor: class ChookDoor
<ChookDoor: class ChookDoor 11.3> =
11.2.1 class ChookDoor: init
<class ChookDoor: init 11.4> =def __init__(self):
self.debug=False
self.lastDoorState='unknown'
now=datetime.datetime.now(timezone)
self.now=now
self.opendelay=120
self.shutdelay=20
self.lastrun=''
self.current='open'
self.sunrise=now # just to initialize
self.sunset=now
self.dooropen=now
self.doorshut=now
11.2.2 class ChookDoor: load
<class ChookDoor: load 11.5> =def load(self):
opendelay=0; shutdelay=0
try:
suntimefile=open(chookFileName,'r')
innow=suntimefile.readline()
self.lastrun=parse('now += (.*)$',innow)
#self.now=self.lastrun
inopdel=suntimefile.readline()
opendelay=parse('opendelay += (.*)$',inopdel)
inshdel=suntimefile.readline()
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
#if self.opendelay!=opendelay:
# print("Opendelay has changed to %d" % (self.opendelay))
#if self.shutdelay!=shutdelay:
# print("Shutdelay has changed to %d" % (self.shutdelay))
pass
11.2.3 class ChookDoor: compute
<class ChookDoor: compute 11.6> =def compute(self):
now=datetime.datetime.now(timezone)
thisday=now.day
today=datetime.date.today()
yesterday=today-datetime.timedelta(days=1)
# NEW sunrise/set calculation
sun = Sun(latitude, longitude)
# Get today's sunrise and sunset in UTC
# have to actually compute yesterday's sunrise, as suntime gets the date wrong!
sunrise = sun.get_local_sunrise_time(yesterday)
sunset = sun.get_local_sunset_time(today)
self.now=datetime.datetime.now(timezone)
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("%H:%M")
self.doorshut=doorshut.strftime("%H:%M")
self.sunrise=sunrise.strftime("%H:%M")
self.sunset=sunset.strftime("%H:%M")
if dooropen.day==now.day: self.whichsrday='today'
else: self.whichsrday='tomorrow'
if doorshut.day==now.day: self.whichssday='today'
else: self.whichssday='tomorrow'
lastState=self.current
11.2.4 class ChookDoor: save
<class ChookDoor: save 11.7> =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
11.2.5 class ChookDoor: Open Chook Door
<class ChookDoor: openDoor 11.8> =def openDoor(self):
if self.debug:
print("(debug) Opening Chook Door")
self.lastDoorState='close'
return
RelayServer.setBitOn(RelayTable['ChookUp'])
time.sleep(2)
RelayServer.setBitOff(RelayTable['ChookUp'])
self.lastDoorState='close'
print("ChookDoor has been opened")
11.2.6 class ChookDoor: Close Chook Door
<class ChookDoor: closeDoor 11.9> =def closeDoor(self):
if self.debug:
print("(debug) Closing Chook Door")
self.lastDoorState='open'
return
RelayServer.setBitOn(RelayTable['ChookDown'])
time.sleep(2)
RelayServer.setBitOff(RelayTable['ChookDown'])
self.lastDoorState='open'
print("ChookDoor has been closed")
11.2.7 class ChookDoor: chookDoor
<class ChookDoor: chookDoor 11.10> =def chookDoor(self,p):
if str(p) in ['open','up','1']:
self.openDoor()
elif str(p) in ['close','shut','down','0']:
self.closeDoor()
else:
raise(BadChook)
11.2.8 class ChookDoor: doorState
<class ChookDoor: doorState 11.11> =def doorState(self):
r=RelayServer.readDoor()
if r=='11':
if self.lastDoorState=='open':
return 'movingdown'
elif self.lastDoorState=='close':
return 'movingup'
return 'door moving'
elif r=='01':
self.lastDoorState='open'
return 'open'
elif r=='10':
self.lastDoorState='close'
return 'closed'
else:
raise(BadChook)
11.2.9 class ChookDoor: handleEvent
<class ChookDoor: handleEvent 11.12> =def handleEvent(self,parms):
ps=parms.split(' ')
device=ps[0]
onoff=ps[1]
try:
self.chookDoor(onoff)
except:
raise(BadChook)
11.2.10 class ChookDoor: run
<class ChookDoor: run 11.13> =def run(self,em,debug):
self.debug=debug
self.load()
self.compute()
#print(self.dooropen)
# strip open and close times to hours:minutes
res=re.match('(\d{2}):(\d{2})',self.dooropen)
op=res.group(1)+':'+res.group(2)
res=re.match('(\d{2}):(\d{2})',self.doorshut)
sh=res.group(1)+':'+res.group(2)
openev=('*',op,'chookdoor','open',self.handleEvent)
em.registerEvent(openev,self.handleEvent)
shutev=('*',sh,'chookdoor','close',self.handleEvent)
em.registerEvent(shutev,self.handleEvent)
The run routine serves only to create the two main chook
door events, opening and closing, registered with the event
manager em passed as a parameter. It is intended to
be called from the event manager at the start of each day.
11.2.11 class ChookDoor: stop
<class ChookDoor: stop 11.14> =def stop(self):
# just print a message for now
print("ChookDoor handler now terminating")
The stop method is called when all events for the day have
been completed. This gives a chance for any event driven
module to clean up and save any relevant information before
the EventManager terminates and no control thread exists any
more.
11.3 ChookDoor: main
<ChookDoor: main 11.15> =def main():
print("Running ChookDoor.main")
chooks=ChookDoor()
chooks.load()
if compute:
chooks.compute()
chooks.save()
eventMan=eventManager.eventManager()
me=('*',chooks.dooropen.strftime("%H%M"),'chookdoor','open','')
eventMan.registerEvent(me,chooks.handleEvent)
me=('*',chooks.doorclose.strftime("%H%M"),'chookdoor','close','')
eventMan.registerEvent(me,chooks.handleEvent)
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))
12. The Garden Steps Lighting
This module follows the same model as the (revised)
ChookDoor module, namely that it defines
GardenSteps.handleEvent method that interfaces to the
EventScheduler.
"GardenSteps.py" 12.1 =#!/home/ajh/binln/python3
from HouseDefinitions import *
from suntime import Sun, SunTimeException
import datetime
class GardenSteps():
def __init__(self):
self.debug=0
self.ontime=''
self.offtime=''
self.ondelay=5
self.offdelay=0
# invoke the suntime routine to find sunrise and sunset
sun = Sun(latitude, longitude)
# Get today's sunrise and sunset in localtime
#sunrise = sun.get_local_sunrise_time() # don't care about sunrise
sunset = sun.get_local_sunset_time()
ontm=sunset+datetime.timedelta(0,0,0,0,int(self.ondelay))
offtm=datetime.time(22,0) # use just fixed time for now - was ...
#sunset+datetime.timedelta(0,0,0,0,int(self.offdelay))
# compute desired on and off times
self.onTime=ontm.strftime("%H:%M")
self.offTime=offtm.strftime("%H:%M")
def switchOn(self):
if not self.debug:
now=datetime.datetime.now()
print("Garden Steps lights are switched on at {}".format(now))
RelayServer.setBitOn(RelayTable['GardenSteps'])
else:
print("(debug) Garden Steps lights are switched on")
return
def switchOff(self):
if not self.debug:
now=datetime.datetime.now()
print("Garden Steps lights are switched off at {}".format(now))
RelayServer.setBitOff(RelayTable['GardenSteps'])
else:
print("(debug) Garden Steps lights are switched off")
return
def switch(self,onoff):
if onoff: self.switchOn()
else: self.switchOff()
return
def handleEvent(self,parms):
ps=parms.split(' ')
device=ps[0]
onoff=ps[1]
if onoff in ['1','on']:
self.switchOn()
elif onoff in ['0','off']:
self.switchOff()
def run(self,em,debug):
self.debug=debug
# register these events
onEv=('*',self.onTime,'gardensteps','on',None)
em.registerEvent(onEv,self.handleEvent)
offEv=('*',self.offTime,'gardensteps','off',None)
em.registerEvent(offEv,self.handleEvent)
# save the on and off times for the web page
stepsTimesf=open('/home/ajh/Computers/House/stepsTimes.txt','w')
stepsTimesf.write("{} (On Time)\n".format(self.onTime))
stepsTimesf.write("{} (Off Time)\n".format(self.offTime))
stepsTimesf.close()
return
def stop(step):
print("GardenSteps handler now terminating")
# not much required as of yet
return
13. The Garden Watering System
This module follows the same model as the (revised)
ChookDoor module, namely that it defines
GardenWater.handleEvent method that interfaces to the
EventScheduler.
A major difference with the GardenSteps module, however,
is that there is more than one sprinkler. Hence the sprinkler
involved has to be passed as a parameter to the event handling
routine, and the on/off routines. Currently there is only one
sprinkler, though: the SouthVegBed. The dispatcher in
the EventScheduler defines a handler for each distinct
water section, even though in each case the handler is the same
(GardenWater.handleEvent). The distinction between the
different watering sections is through the device name, passed
as a parameter through the handleEvent methos.
"GardenWater.py" 13.1 =#!/home/ajh/binln/python3
from HouseDefinitions import *
from suntime import Sun, SunTimeException
import datetime
import re
class GardenWater():
def __init__(self):
self.debug=0
self.onTime='09:00'
self.offTime='09:15'
def switchOn(self,sprinkler):
if not self.debug:
now=datetime.datetime.now()
print("Garden Water Sprinkler {} turned on at {}".format(sprinkler,now))
RelayServer.setBitOn(RelayTable[sprinkler])
else:
print("(debug) Garden Water Sprinkler {} turned on".format(sprinkler))
return
def switchOff(self,sprinkler):
if not self.debug:
now=datetime.datetime.now()
print("Garden Water Sprinkler {} turned off at {}".format(sprinkler,now))
RelayServer.setBitOff(RelayTable[sprinkler])
else:
print("(debug) Garden Water Sprinkler {} turned off".format(sprinkler))
return
def switch(self,onoff,sprinkler):
if onoff: self.switchOn(sprinkler)
else: self.switchOff(sprinkler)
return
def handleEvent(self,parms):
print("Garden Watering parameters are {}".format(parms))
ps=parms.split(' ')
device=ps[0]
onoff=ps[1]
if onoff in ['1','on']:
self.switchOn(device)
elif onoff in ['0','off']:
self.switchOff(device)
def run(self,em,debug):
self.debug=debug
# register these events
onEv=('*',self.onTime,'gardenwater','SouthVegBed on',None)
em.registerEvent(onEv,self.handleEvent)
offEv=('*',self.offTime,'gardenwater','SouthVegBed off',None)
em.registerEvent(offEv,self.handleEvent)
# save the on and off times for the web page
waterTimesf=open('/home/ajh/Computers/House/waterTimes.txt','w')
waterTimesf.write("{} (On Time for {})\n".format(self.onTime,'SouthVegBed'))
waterTimesf.write("{} (Off Time for {})\n".format(self.offTime,'SouthVegBed'))
waterTimesf.close()
return
def stop(step):
print("GardenWater handler now terminating")
# not much required as of yet
return
14. The Web Interface
The web interface is cgi application running on the house
computer ouyen, and providing a conventional web page via
a port 80 call (the http
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.
14.1 The house.py cgi application
"house.py" 14.1 =#!/home/ajh/binln/python3
import cgi
import datetime
import HouseMade
import os
now=datetime.datetime.now()
nowstr=now.strftime("%Y%m%d:%H%M")
import cgitb
cgitb.enable()
print("Content-type: text/html\n\n")
form=cgi.FieldStorage()
#print(form)
remadr=os.environ['REMOTE_ADDR']
server=os.environ['SERVER_NAME']
#print("%s@%s: house arguments=%s" % (server,remadr,form))
#print(os.environ)
page=HouseMade.house(remadr,server,form)
print(page)
This cgi script simply collects a few parameters, and then
passes control to the house interface in the
HouseMade module. The latter module has the
responsibility of generating the web page, which is returned
as a string to be printed (rendered) by this cgi script.
14.2 The HouseMade module
"HouseMade.py" 14.2 =
14.2.1 The house interface
<HouseMade: define the house interface 14.3> =
14.2.2 Get Local Information
<HouseMade: get local information 14.4> =# Firstly, chook relevant times
import ChookDoor
chooks=ChookDoor.ChookDoor()
chooks.compute()
#chooks.load()
localrise=chooks.sunrise
localset=chooks.sunset
gateopentime=chooks.dooropen
gateshuttime=chooks.doorshut
import GardenSteps
steps=GardenSteps.GardenSteps()
stepsOn=steps.onTime
stepsOff=steps.offTime
dayOpen=chooks.whichsrday.capitalize()
dayShut=chooks.whichssday
import GardenWater
water=GardenWater.GardenWater()
waterOn=water.onTime
waterOff=water.offTime
localinfo=''
14.2.3 Get Relay Information
<HouseMade: get relay information 14.5> =RelayState=RelayServer.getState()
# here process any switching requests
# respond to any argument requests - can only do if RPC server present
active=False
if args and HouseDefinitions.RelayServerGood:
currState=RelayServer.getState()
for relay in RelayNames:
if relay in args:
active=True
bitNo=RelayTable[relay]
newState=args[relay].value
if newState in ['off','on']:
doWhat={'off':RelayServer.setBitOff,'on':RelayServer.setBitOn}
change=doWhat[newState](bitNo)
#print("change bit %d(%s) to %s" % (bitNo,relay,newState))
else:
# start timer with time == newstate
timerCount=int(newState)
RelayServer.start(bitNo,timerCount)
#print("timer started for bit %d(%s) for %d" % \
# (bitNo,relay,timerCount))
# just to confirm any changes
RelayState=RelayServer.getState()
#print(RelayState);time.sleep(5)
# deal with chook door state
#curdoorstate=RelayServer.readDoor()
#chookdoorlabel='open' # until proved otherwise
#if curdoorstate[0]=='0': # door is now up, turn off ChookUp
# RelayServer.setBitOff(RelayTable['ChookUp'])
# chookdoorlabel='open' # established
#elif curdoorstate[1]=='0': # door is now down, turn off ChookDown
# RelayServer.setBitOff(RelayTable['ChookDown'])
# chookdoorlabel='closed'
#else:
# if RelayState[RelayTable['ChookDown']]:
# chookdoorlabel='movingdown'
# active=True
# if RelayState[RelayTable['ChookUp']]:
# chookdoorlabel='movingup'
# active=True
chookdoorlabel=chooks.doorState()
# just to confirm any changes
RelayState=RelayServer.getState()
currentcircuits=[]
for i in range(NumberOfRelays):
if RelayState[i]:
currentcircuits.append(RelayNames[i])
if len(currentcircuits) > 1:
currentcircuits = "the " + ', '.join(currentcircuits[:-1]) + " and " + currentcircuits[-1]
elif len(currentcircuits) > 0:
currentcircuits = "the " + currentcircuits[0]
else:
currentcircuits = "no"
relayStateStr='''
<p>
Currently %(currentcircuits)s circuits are on.
</p>
''' % vars()
#relayStateStr+='''
# 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>.
# ''' % vars(HouseDefinitions)
The relay information is currently provider by the server
running on kerang, which at the moment only relates
to the chook door opening and closing. This will be made
more generic in the near future.
14.2.4 Get Events Information
<HouseMade: get events information 14.6> =eventsInfo=''
i=0; evlist=[]
while True:
ev=EventServer.getEvent(i)
if ev: evlist.append(ev);i+=1
else: break
if i>0:
now=datetime.datetime.now()
nowTime=now.strftime("%H:%M")
eventsInfo+='<h3>Previously scheduled events for today</h3>\n<table>\n'
nextHead='</table><h3>{} Now</h3><table>'.format(nowTime)
for e in evlist:
if e['time']>nowTime:
eventsInfo+=nextHead
nextHead=''
eventsInfo+='<tr>\n'
eventsInfo+='<td width="100px">{}</td>'.format(e['time'])
eventsInfo+='<td width="200px">{}</td>'.format(e['device'])
eventsInfo+='<td width="100px">{}</td>'.format(e['operation'])
eventsInfo+='</tr>\n'
eventsInfo+='<table>\n'
else:
eventsInfo += '<p>No events found</p>'
14.2.5 Legacy Code
<HouseMade: legacy code for HouseMade.house 14.7> =(aimtemp,onoff) = (ThermostatSetting,'off') #garedelyon.getHeating()
res=0
onoffcolor='blue'
# legacy code - will be reinstated some day
if onoff=='on': onoffcolor='red'
import os,string
###################
# 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
<HouseMade: Relay Control 14.8>
################### Water Storage
tanksection='' # tank([])
################### Solar Power
solarsection='' # solar([])
################### Climate
weathersection='' # weather([]) ##############################
###################
14.2.6 The Relay Information section
<HouseMade: Relay Control 14.8> =RelayServerGood=True
if RelayServerGood:
row="<tr><th>Name</th><th>bit No</th><th>On/Off</th>"
row+="<th>Timer</th><th colspan='8'>Run For</th></tr>\n"
for key in sorted(RelayTable, key=RelayTable.get):
bitNo=RelayTable[key]
thisBit=RelayState[bitNo]
thisState=['off','on'][thisBit]
newState=['on','off'][thisBit]
thisColour=['lightblue','red'][thisBit]
row+=' <form action="http://{}/~ajh/cgi-bin/house.py?action=switch" method="post">\n'.format(CENTRAL)
row += ' <tr bgcolor="%s">\n' % (thisColour)
row += ' <td>%s</td>\n' % (key)
row += ' <td bgcolor="%s">%s</td>\n' % (thisColour,bitNo) # bit number
row += ' <td bgcolor="%s">\n' % (thisColour)
row += ' <input type="hidden" name="state" value="{}"/>\n'.format(newState)
row += ' <input type="hidden" name="relay" value="{}"/>\n'.format(bitNo)
row += ' <button name="%s" value="%s" type="submit">\n' % (key,newState)
row += ' %s\n </td>\n' % (thisState)
timeLeft=RelayServer.getTimer(bitNo)
if timeLeft>0: active=True
row += ' <td bgcolor="%s" width="50px">%d</td>\n' % (thisColour,timeLeft)
for t in [30,60,120,300,600,1200,1800,3600]:
if t<60:
if key[0:5]=='Chook': t=45
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>\n' % (key,t1,buttontxt)
row += " </tr>\n"
row += " </form>\n"
################### Chook Door
chookmode=''
if chookdoorlabel == 'closed':
chookdoorcolour="lime"
elif chookdoorlabel == 'open':
chookdoorcolour="orangered"
elif chookdoorlabel == 'movingdown':
chookdoorcolour="greenyellow"
chookmode='style="fade"'
elif chookdoorlabel == 'movingup':
chookdoorcolour="pink"
chookmode='style="fade"'
else:
chookdoorcolour="grey"
row += '<tr bgcolor="%s">\n' % (chookdoorcolour)
row += ' <td>ChookDoor</td>'
row += '<td colspan="11" align="center" %s>%s</td>\n' % (chookmode,chookdoorlabel)
row += '</tr>\n'
relaycontrol="""
<table border="1">
%(row)s
</table>
""" % vars()
else:
relaycontrol='<p>No relay information available</p>'
14.2.7 define the Generate Weather Data routine
<HouseMade: define the Generate Weather Data routine 14.9> =def weather(args):
WServer='http://%s:5000/weather' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
MAXMINFILE='/home/ajh/logs/terang/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"
WServer='http://%s:5000/weather' % (CENTRAL) # WeatherServer
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">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.
14.2.8 define the Generate Solar Data routine
<HouseMade: define the Generate Solar Data routine 14.10> =def solar(args):
SServer='http://%s:5000/solar' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
in_Ah = 0.0 # Central.getSolar(20)
out_Ah = 0.0 # Central.getSolar(24)
solaramps = 0.0 # Central.getSolar(32)
solarbatteryvolts = 0.0 # Central.getSolar(34)*0.1+15
solarpower = 0.0 # solaramps*solarbatteryvolts
percentsolar = 0.0 # 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.
14.2.9 define the Generate Tank Data routine
<HouseMade: define the Generate Tank Data section 14.11> =def tank(args):
TServer='http://%s:5000/tank' % (CENTRAL)
MServer='http://%s:5000/house' % (CENTRAL)
# get tank data
tankvolume=0.0 # RelayServer.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.
14.2.10 Collect Date and Time Data
<HouseMade: collect date and time data 14.12> =# 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.
14.2.11 Check the Client Connection
<HouseMade: check client connection 14.13> =if 'SSH_CONNECTION' in os.environ:
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'
if DEBUG:
print(os.environ)
print(clientIP)
clientIP='10.0.0.3'
res=re.match('10\.0',clientIP)
if res: # only allow connections from local network
clientOK=True
else: # non-local network, disallow
clientOK=False
sys.stderr.write("ATTEMPT TO ALTER HOUSE SETTINGS\n")
sys.exit(1)
if not HouseDefinitions.RelayServerGood:
print("<p>Cannot talk to the RelayServer - have you started it?</p>")
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. (Currently not implemented.)
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. All others pay money, and their
names are taken.
14.2.12 Generate the Web Page Content
<HouseMade: generate the web page content 14.14> ="""
<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="%(serverlink)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://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> and
<a href="http://www.bom.gov.au/products/IDV60901/IDV60901.95867.shtml">
temperatures</a>.
%(localinfo)s
%(relaycontrol)s
%(relayStateStr)s
%(eventsInfo)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.
14.2.13 Make the Temperature Panel (not currently used)
<house make temperature panel 14.15> =# 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.
14.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" 14.16 = **** File not generated!#! /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 14.17>
def
heating(logMsg,remadr,args):
DEBUG=False
active=False
<HouseMade: collect date and time data 14.12>
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 14.18>
<Web: heating: build widths for web page table 14.19>
# 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()
14.3.1 Define the heatingData Class
<Web: define the heatingData class 14.17> =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()
Chunk referenced in 14.16
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.
14.3.2 Collect Parameters and Update
<Web: heating: collect parameters and update 14.18> =# 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
Chunk referenced in 14.16
14.3.3 Build Widths for Web Page Table
<Web: heating: build widths for web page table 14.19> =# 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
Chunk referenced in 14.16
<check client IP address OK 14.20> =if 'REMOTE_ADDR' in os.environ:
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.
15. 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.
15.1 The C Weather Monitor Program
(Details to be recorded)
15.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" 15.1 = **** File not generated!#!/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)))
15.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" 15.2 = **** File not generated!import datetime
import re
from wx200 import *
STATEFILE='/home/ajh/logs/terang/wxState.txt'
MAXMINFILE='/home/ajh/logs/terang/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 today in maxmintable:
(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()
16. The Heating System
16.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 >.
"AdjustHeat.py" 16.1 = **** File not generated!
<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=20.0 # 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
RelayServer.setBitOn(bitNo)
pass
if turn=='off':
# turn heating off here
RelayServer.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.
16.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" 16.2 = **** File not generated!#! /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.
16.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" 16.3 = **** File not generated!
<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)
17. The Tank System
Define here those program components concerned (solely) with
recording and suppling water tank information.
17.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
17.2 Start the Tank Logging
"startTankLog.sh" 17.1 = **** File not generated!
<edit warning 2.1>
LOGDIR='/home/ajh/logs/terang'
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.
17.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" 17.2 = **** File not generated!#!/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/terang/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/terang/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.
18. 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.
18.1 logsolar.py
"logsolar.py" 18.1 = **** File not generated!#!/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.
18.2 solar.py
Provide definitions and access functions for the solar
controller.
"solar.py" 18.2 = **** File not generated!#!/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.
19. The House Computer
19.1 The Current State Interface
"currentState.py" 19.1 = **** File not generated!#!/usr/bin/python
Debug=False
STATEFILE='/home/ajh/logs/terang/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.
19.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" 19.2 = **** File not generated!#!/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 19.3>
server.register_function(getTemps, 'getTemps')
<HouseData define maxminTemp 19.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)).
19.2.1 HouseData define getTemps
<HouseData define getTemps 19.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)
19.2.2 HouseData define maxminTemp
<HouseData define maxminTemp 19.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
19.3 The startHouseData.sh script
"startHouseData.sh" 19.5 = **** File not generated!#!/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
20. The Event Manager
This section has been superceded by the EventScheduler
module, and has its code sections commented out.
This was a new section in version 2, and represented a change in
thinking on how best to do the timing of events in the house
environment. One factor is the ability to provide trace-back
analysis when things go wrong, and to maintain a permanent
record of what has been happening. This means migrating a
significant amount of data into files, rather than hard-coding
it into programs and program structures.
The model is this:
-
The event manager runs continuously, normally asleep, and
once a minute wakes up to see whether an event is ready to
be invoked.
-
Each module requiring some timed activity must register its
event with the event manager, along with the timing details,
and a handler for when the event occurs.
-
When an event's time is reached, the event manager passes
control to the event handler, which handles the event before
returning control (although it could spawn a separate thread
before returning control if it needs a significant amount of
time).
-
The handler may do various things, including re-registering
itself with the event handler for a future event.
"EventManager.py" 20.1 =#!/home/ajh/binln/python3
import datetime
import getopt
import time
import re
import sys
import ChookDoor
import GardenSteps
import GardenWater
from HouseDefinitions import *
debug=0 ; testing=0 ; fastmode=0
verbose=0
NEFname="/home/ajh/Computers/House/events.txt"
dayp ="(Sun\w*|Mon\w*|Tue\w*|Wed\w*|Thu\w*|Fri\w*|Sat\w*|\*) +"
datep="(|\d* +|{})".format(dayp) # groups 1,2
timep="(\d+:\d+)(@\d+|-\d+:\d+)?" # groups 3,4
object="(\w+)" # groups 5
parms="(\(.*\))" # groups 7
list="([\w ]*)" # groups 8
eventPat="{}{} +{}({}|{})".format(datep,timep,object,parms,list) # groups 6
chookdoor=ChookDoor.ChookDoor()
gardensteps=GardenSteps1.GardenSteps()
gardenwater=GardenWater1.GardenWater()
def strEvent(ev):
if not ev: return 'None'
(d,t,o,p,h)=ev
s=''
s+="{} ".format(d)
s+="{} ".format(t)
s+="{} ".format(o)
s+="{} ".format(p)
s+="{} ".format(h)
return s
<Event Manager: Event class 20.2>
<Event Manager: Clock 20.3>
<Event Manager: class eventManager 20.5>
<Event Manager: main 20.4>
if __name__=='__main__':
(vals,path)=getopt.getopt(sys.argv[1:],'dftvV',
['debug','fast','testing','verbose','version'])
for (opt,val) in vals:
if opt=='-d' or opt=='--debug':
debug=1 ; testing=1 ; fastmode=1
if opt=='-f' or opt=='--fastmode':
fastmode=1
if opt=='-t' or opt=='--testing':
testing=1
if opt=='-v' or opt=='--verbose':
verbose=1
if opt=='-V' or opt=='--version':
print(version)
sys.exit(0)
if len(path)!=0:
usage()
sys.exit(1)
main(testing,fastmode)
The eventManger module provides a class to do all the
management of events in the house system. It no longer
maintains a file of saved events.
Thinking aloud bit: I have generated a bit of self confusion
over what modes of operation should be implemented. Here is a
table of what should be happening:
|
No Testing |
Testing, CLI "-t" |
No fast mode |
Normal operation, normal speed |
Testing operation, normal speed |
Fast mode, CLI "-f" |
Normal operation, fast speed |
Testing operation, fast speed (CLI "-d") |
Because debugging is the more commonly thought-of mode of
testing, it is actually a shorthand for setting both the
testing and fastmode simultaneously. It is
(currently) unused otherwise, but could be used in future to
control other debug information.
20.1 The Event class
<Event Manager: Event class 20.2> =class Event():
def __init__(self):
self.event=None
self.day=''
self.time='00:00'
self.operation=''
self.parms=''
self.handle=None
20.2 The Clock
<Event Manager: Clock 20.3> =def
Clock(em,fastmode=False):
if fastmode: print("Clock is running in fastmode")
interval=60
now=datetime.datetime.now()
nowTime=now.strftime("%H:%M")
needEvent=True
while True:
now=datetime.datetime.now()
if fastmode:
# in fast mode, time runs 60 times faster
nowTime=nowTime
else:
nowTime=now.strftime("%H:%M")
if needEvent:
# get next event
(ev,secs,num)=em.getNextEvent()
needEvent=False
if ev:
strEv=strEvent(ev)
print("Loaded next event, it is {}".format(strEv))
nextEventFile=open(NEFname,'w')
nextEventFile.write("{} {}\n".format(num+1,strEv))
nextEventFile.close()
else:
nextEventFile=open(NEFname,'w')
nextEventFile.write("{} {}\n".format(0, 'nothing'))
nextEventFile.close()
if ev:
# check time
(d,t,o,p,h)=ev
if t == nowTime:
em.handleEvent(ev)
needEvent=True
else:
print("Run out of events, terminating at {}".format(now))
return
seconds=now.second
if fastmode:
interval=1
h=int(nowTime[0:2]) ; m=int(nowTime[3:5])
m+=1
if m==60:
h+=1
m=0
nowTime="{:02d}:{:02d}".format(h,m)
print("fasttime is now {}".format(nowTime))
else:
interval=60-seconds
if now.second==0:
print("time is now {}".format(now))
state=RelayServer.getState()
logMsg("current state is {}".format(state))
time.sleep(interval)
The Clock routine is the heart of the Event Manager.
It runs continously once the Event Manager has been
initialized, invoking the handler for each event as the
appropriate time arrives. It terminates only when there are
no events left (which should be before the end of the day, but
this is an assumption that may need to be changed).
The fastmode flag causes the clock to run 60 times
faster, i.e., 1 second real time represents 1 minute of
simulated time. This is useful for debugging.
20.3 Event Manager: main
<Event Manager: main 20.4> =def main(
testing=False,
fastmode=False):
now=datetime.datetime.now()
nowtime=now.time()
e=eventManager()
for ev in e.eventsList:
print(strEvent(ev))
order=e.sortEvents()
e.eventsList=order
chookdoor.run(e,testing)
gardensteps.run(e,testing)
gardenwater.run(e,testing)
print("\nIn sorted order:")
for ev in order:
print(strEvent(ev))
if
fastmode or not
testing:
Clock(e,
fastmode)
else:
i=0
while True:
#print(strEvent(e.nextEvent))
(nexte,diff,togo)=e.getNextEvent()
if not nexte: break
(d,tim,o,parms,handle)=nexte
minutes=diff // 60
hours=int(minutes // 60)
minutes=int(minutes % 60)
print("The next scheduled event is {}".format(nexte))
print("It will occur in {} seconds, at {:02d}:{:02d}".format(diff,hours,minutes))
print("There are another {} events after this".format(togo))
#for j in range(i+1,len(e.eventsList)):
# print(strEvents(e.eventsList[j]))
if handle:
print("\n{} Calling handler {} with parms {}".format(tim,handle,parms))
handle(parms)
print()
if i > 11: break
i+=1
pass
chookdoor.stop()
gardensteps.stop()
print("No more events today, terminating EventManager")
main has two parameters testing, and
fastmode. testing is what is normally thought
of as a debug mode, and disables any actual activation of
controlled operations, printing a test message instead.
fastmode gives finer control over testing. When set
False, events are scheduled and handled in real time, as
determined by the Clock routine. When set True, events
are processed in sequence, with no waiting between each event,
other than the raw time taken by each event.
In the True scenario, if more time is required between each
event, then either include a specific 'sleep' in each event,
or add a generic 'sleep' in the while True loop
above.
20.4 The Event Manager class
<Event Manager: class eventManager 20.5> =
20.4.1 the sortEvents method
<Event Manager: sort events 20.6> =def sortEvents(self):
def sortkey(ev):
return ev[1]
evlist=self.eventsList
evlist.sort(key=sortkey)
self.isordered=True
return evlist
sortEvents sorts the events into chronological order,
assuming (for now) that all the events are taking place today,
and determines what is the next event to occur. This is so that
a timer thread may be started, which will wake up when the event
is to take place. To this end, the time difference between now
and the next event time is computed.
20.4.2 the getNextEvent method
<Event Manager: get next event 20.7> =def getNextEvent(self):
now=datetime.datetime.now()
# comment out the next line for testing
now=datetime.datetime.now()
nowtime=now.time()
if not self.isordered:
order=self.sortEvents()
self.eventsList=order
nowTime=now.strftime("%H:%M")
listLength=len(self.eventsList)
nextEventNumber=self.nextEvent
while nextEventNumber < listLength:
print("Processing event number {}".format(nextEventNumber))
ev=self.eventsList[nextEventNumber]
self.nextEvent+=1
print("{}: Event: {}".format(nowTime,strEvent(ev)))
(d,t,o,p,h)=ev
if nowTime > t:
print("skipping event {}, scheduled time {} has passed".format(nextEventNumber,t))
nextEventNumber=self.nextEvent
continue # event has passed, skip to next one
tm=datetime.datetime.strptime(t,"%H:%M")
tm=tm.time()
t1=datetime.timedelta(hours=tm.hour, minutes=tm.minute)
t2=datetime.timedelta(hours=nowtime.hour, minutes=nowtime.minute)
secs=(t1-t2).total_seconds()
togo=len(self.eventsList)-nextEventNumber-1
print("End of nextEvent, numEvents={}, i={}, togo={}".format(listLength,self.nextEvent,togo))
return(ev,secs,togo)
# No more events, nowTime > last event time, so return None
print("getNextEvent runs out of events at time {}".format(nowTime))
return(None,0,0)
getNextEvent scans the current list of events,
looking for the next event from the current time. If found,
that event is returned, along with the number of seconds to
go before the event, and the number of events scheduled
after this event.
Otherwise, None is returned to indicate that all
events have been scheduled.
The EventManager entity self.nextEventNumber keeps
track of which number the next event in the list of events
should be examined. This is done to speed up the search.
20.4.3 Handle an event
<Event Manager: handle event 20.8> =def handleEvent(self,ev):
#print(strEvent(ev))
(d,t,o,p,h)=ev
key="{}".format(o.strip())
if h:
print("Now handling event {}".format(strEvent(ev)))
h(p)
else:
print("Unregistered action {} at {}".format(key,t))
20.4.4 Register an Event
<Event Manager: register event 20.9> =def registerEvent(self,ev,handle):
(d,eventTime,eventName,eventParms,h)=ev
if not self.isordered:
order=self.sortEvents()
self.eventsList=order
# now scan list in chronological order
for i in range(len(self.eventsList)):
evn=self.eventsList[i]
(d,t,o,p,h)=evn
#print("checking register time {} against {}".format(eventTime,t))
if eventTime == t:
#print("Have two events at the same time {}/{}".format(eventTime,t))
#print("Event names are {}/{}".format(eventName,o))
# check if duplicate event
if o == eventName:
# is the same, replace old event at this time
#print("register {} at same time as {}".format(strEvent(ev),strEvent(evn)))
self.eventsList[i]=('*',eventTime,eventName,eventParms,handle)
return
elif eventTime < t:
# event goes in list before this element
#print("Event {} entered before {}".format(strEvent(ev),strEvent(evn)))
self.eventsList.insert(i,('*',eventTime,eventName,eventParms,handle))
return
# reached the end, insert here
#print("Event {} entered at end".format(strEvent(ev)))
self.eventsList.append(('*',eventTime,eventName,eventParms,handle))
21. Test Programs
21.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" 21.1 =
<edit warning 2.1>
import xmlrpc.client
import HouseDefinitions
RelayServer=HouseDefinitions.RelayServer
print("options are:")
print(" RelayServer.getState()")
print(" RelayServer.setState([0,0,...]) # 12-element vector of 0/1")
print(" RelayServer.setBitOn(bitnumber) # bit number is an integer (0-11)")
print(" RelayServer.setBitOff(bitnumber) # bit number is an integer (0-11)")
print()
#print(" RelayServer.getHeating()")
#print(" RelayServer.setHeating(float,'on'/'off')")
#print(" RelayServer.getSolar(n) # n is register number (32 is input amps)")
#print(" RelayServer.getTemps()")
#print(" RelayServer.getTank()")
#print(" RelayServer.maxminTemp()")
#print()
#print("for example,")
#print(" RelayServer.getHeating()=%s" % (RelayServer.getHeating()))
#print(" RelayServer.getState()=%s" % (s.getState()))
print
Note that the data logging operations are not currently
available.
22. 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
23. Installing and Starting the HouseMade Software
23.1 Introduction
This has always been a somewhat fraught area of development
since the earliest versions of this software. That is largely
due to the variety of both hardware and software in use, and
the various idiosyncracies involved. This section attempts to
address these issues.
This is a list of all the processes that need starting:
-
The Beagle subsystem software, involving a relay driver
BeagleDriver.py, a relay/chook door state request
server BeagleServer, and the BeagleBone GPIO setup
AJH-GPIO-Relay.dts.
-
The House Data Logging Computer relay interface
RelayServer.py.
-
chook door (the need for this as a standalone is currently
in question).
23.2 Details
23.2.1 Start the Beagle Server
The BeagleServer (aka the chook door server) provides an RPC
interface to a) interrogate whether or not the chook door is
closed, according to the proving microswitch, and b) drive
the relay switching.
The proving circuits are 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 (kerang), and can
be started with the make call:
make start-beagle
This make call just invokes the following script (after
making sure that its code is up-to-date) on the BeagleBone
(known by its network name kerang). The script can
also be invoked directly on the kerang machine from the
command line.
"startBeagleServer.sh" 23.1 =#!/bin/bash
LOGDIR='/home/ajh/logs/kerang'
HOUSE='/home/ajh/Computers/House'
BIN=${HOME}/bin
# collect any previous instance
ps aux | grep "BeagleServer.py" | grep -v grep | awk '{print $2}' >${LOGDIR}/beagleServerPID
# remove any previous instances
if [ -f ${LOGDIR}/beagleServerPID ] ; then
for p in `cat ${LOGDIR}/beagleServerPID` ; do
kill -9 `head ${LOGDIR}/beagleServerPID`
done
rm ${LOGDIR}/beagleServerPID
fi
# start the new instance
/home/ajh/binln/python /home/ajh/Computers/House/BeagleServer.py >>~/logs/kerang/BeagleServer.log &
# record the new instance
ps aux | grep "BeagleServer.py" | grep -v grep | awk '{print $2}' >>${LOGDIR}/beagleServerPID
If this doesn't work, or you need a more direct interface,
then in a window on the kerang machine itself:
kerang $ /home/ajh/Computers/House/BeagleServer.py
will run the server in that window. Note that as this
program runs continuously, it should be started in a
separate terminal window (which can then hidden from view
but left running). This mode has the advantage that any
output from the server can be seen immediately in that
window, rather than having to examine the logfile (as
required by the start Beagle methods above).
23.2.2 Relay Server
The RelayServer runs continously on the House Data Logging
computer (currently set as terang. It can be started
with a make call:
make start-relayserver
If this doesn't work, or you need a more direct interface, then
(any machine) $ ssh terang /home/ajh/Computers/House/RelayServer.py
Since this program runs continuously, it should be started
in a separate terminal window, which is then hidden from
view, but left running.
24. The Cron Jobs
Currently no cron jobs are required.
25. Makefile
"Makefile" 25.1 =default=HouseMade
CGI = ${HOME}/www/cgi-bin
HOUSE = ${HOME}/Computers/House
BIN = ${HOME}/bin
CENTRAL = terang
CENTRALHOUSE = $(CENTRAL):$(HOME)/Computers/House
NEWPORT = newport.local
NEWPORTHOUSE = $(NEWPORT):$(HOME)/Computers/House
SPENCER = spencer.local
SPENCERHOUSE = $(SPENCER):$(HOME)/Computers/House
OUYEN = ouyen.local
OUYENHOUSE = $(OUYEN):$(HOME)/Computers/House
TERANG = terang
TERANGHOUSE = $(TERANG):$(HOME)/Computers/House
XSLLIB = /home/ajh/lib/xsl
XSLFILES = $(XSLLIB)/lit2html.xsl $(XSLLIB)/tables2html.xsl
WEBSOURCE = ${HOME}/www/computing/sources/house
CGIFILES = house.py HouseMade.py ChookDoor.py ChookDoor.py RelayServer.py \
GardenSteps.py GardenWater.py Events.py
SCP = /usr/bin/scp
RSYNC = /usr/bin/rsync
include ${HOME}/etc/MakeXLP
GenFiles = RelayServer.py \
house.py HouseMade.py HouseDefinitions.py \
Events.py EventServer.py EventScheduler.py \
ChookDoor.py GardenSteps.py GardenWater.py \
eventEditor.py
${GenFiles}:HouseMade.tangle
install: install-central
# install-central installs all executable files into the House directory
# on CENTRAL, whatever that it
install-central: make-executable install-eventedit install-events install-house
@if [ $(CENTRAL) = $(NEWPORT) ] ; then \
rsync -auv $(CGIFILES) $(NEWPORT):/home/ajh/public_html/cgi-bin/ ; \
fi
@if [ $(CENTRAL) = $(SPENCER) ] ; then \
rsync -auv $(CGIFILES) $(SPENCER):/home/ajh/public_html/cgi-bin/ ; \
fi
@if [ $(CENTRAL) = $(TERANG) ] ; then \
rsync -auv $(CGIFILES) $(TERANG):/home/ajh/public_html/cgi-bin/ ; \
fi
<Makefile: RelayServer makes 25.2>
<Makefile: BeagleBone makes 25.3>
make-executable: HouseMade.tangle
chmod 755 house.py RelayServer.py RelayControl.py
chmod 755 EventServer.py
chmod 755 EventScheduler.py
install-eventedit: HouseMade.tangle
chmod 755 eventEditor.py
cp -p eventEditor.py /home/ajh/public_html/cgi-bin/
touch install-eventedit
install-events: HouseMade.tangle
${RSYNC} HouseDefinitions.py ${CENTRAL}:/home/ajh/public_html/cgi-bin/
cp -p Events.py /home/ajh/public_html/cgi-bin/
touch install-events
install-house: HouseMade.tangle
${RSYNC} house.py ${CENTRAL}:/home/ajh/public_html/cgi-bin/
${RSYNC} HouseMade.py ${CENTRAL}:/home/ajh/public_html/cgi-bin/
${RSYNC} Events.py ${CENTRAL}:/home/ajh/public_html/cgi-bin/
touch install-house
HouseMade.tangle HouseMade.xml: HouseMade.xlp
xsltproc --xinclude $(XSLLIB)/litprog.xsl HouseMade.xlp >HouseMade.xml
touch HouseMade.tangle RelayControl.tangle
HouseMade.html: HouseMade.xml $(XSLFILES)
xsltproc --xinclude $(XSLLIB)/lit2html.xsl HouseMade.xml >HouseMade.html
html: HouseMade.html
pdf: HouseMade.pdf
###########################
# MAKEFILE #
###########################
makefile: Makefile.tangle
#############################
# END OF INSTALLATION STUFF #
#############################
executable: house.py timer.py
chmod 755 house.py timer.py
all: HouseMade.html
clean: litclean
-rm $(GenFiles)
25.1 RelayServer Makes
<Makefile: RelayServer makes 25.2> =install-relayserver: HouseMade.tangle RelayControl.tangle make-executable
$(SCP) RelayServer.py RelayControl.py $(CENTRALHOUSE)/
touch install-relayserver
install-startRelay: HouseMade.tangle install-relay make-executable
$(SCP) startRelayServer.sh $(CENTRALHOUSE)/
touch install-startRelay
# start the House Computer Relay Server
start-relayserver: HouseMade.tangle install-startRelay
ssh $(CENTRAL) $(HOME)/Computers/House/startRelayServer.sh
touch start-relayserver
25.2 BeagleBone Makes
<Makefile: BeagleBone makes 25.3> =install-beagle: HouseMade.tangle
chmod 755 BeagleServer.py
chmod 755 startBeagleServer.sh
$(SCP) BeagleServer.py BeagleDriver.py $(TERANGHOUSE)/
$(SCP) BeagleClient.py startBeagleServer.sh $(TERANGHOUSE)/
ssh $(TERANG) mv $(HOUSE)/startBeagleServer.sh $(HOME)/bin/
touch install-beagle
start-beagle: HouseMade.tangle install-beagle
ssh terang $(HOME)/bin/startBeagleServer.sh
touch start-beagle
25.3 Makefile: install bastille
<Makefile: install bastille 25.4> =##############################
# 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-everyMinute: HouseMade.tangle
chmod 755 EveryMinute.sh
$SCP EveryMinute.sh ${BASTILLE}/
touch install-everyMinute
install-html: HouseMade.html
if /usr/bin/diff HouseMade.html ${WEBSOURCE}/HouseMade.html; then \
cp HouseMade.html ${WEBSOUCE}/HouseMade.html ;\
fi
install-startRelay: HouseMade.tangle install-relay
chmod 755 startRelayServer.sh
$SCP startRelayServer.sh $(CENTRAL)/
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-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
26. Document History
20200715:133241 |
ajh |
2.0.0 |
Major transformation to deal with complete house renovation,
along with the decommissioning of most of the system
components. First task: get RelayServer.py working.
|
20200716:103919 |
ajh |
2.0.1 |
RelayServer.py now working. Starting work on the flask
section.
|
20200813:093446 |
ajh |
2.1.0 |
start work on timed events, notably chicken door |
20200902:101223 |
ajh |
2.1.1 |
extensive literate programming rework, little functional change
|
20200904:164010 |
ajh |
2.1.2 |
added RelayControl and incorporated it into house.py,
maintain functionality although other stubs (solar, weather,
etc.) have been removed for now.
|
20200908:181003 |
ajh |
2.1.3 |
bring the BeagleBone system onboard |
20200916:174933 |
ajh |
2.1.4 |
Added GardenSteps module, re-arranged many of the sections,
and removed unused legacy code. First operational version
for event managing.
|
20200920:142356 |
ajh |
2.1.5 |
Added EventServer, which was a failed experiment, due to an
inability to adequate share data across a multiprocessing
context. Need to go back to CS3203! What this means is
that the code for EventServer.py will be left unused
for now, to be removed at some stage in the future
|
20200926:095443 |
ajh |
2.1.6 |
removed code for EventServer |
20200927:150042 |
ajh |
2.1.7 |
removed redundant code in getNextEvent
|
20201004:125117 |
ajh |
2.1.8 |
tweaks to provide more logging information |
20201011:181914 |
ajh |
2.2.0 |
add garden water module, first attempt |
20201207:112418 |
ajh |
2.2.1 |
Changed ChookDoor, GardenSteps, GardenWater modules to be
ChookDoor1, GardenSteps1, GardenWater1 to avoid conflict
with revised system EventServer.
|
20201213:133903 |
ajh |
3.0.0 |
merge of HouseMade 2.2.1 and EventServer 1.0.4, otherwise,
little change
|
20201213:154451 |
ajh |
3.0.1 |
migrated separate Makefile.xlp into this file |
20201214:165523 |
ajh |
3.0.2 |
Updated the History section. |
20201221:131301 |
ajh |
3.0.3 |
revisions in moving suite to Terang |
20201229:133233 |
ajh |
3.0.4 |
various tidy-ups of literate code |
<current version 26.1> = 3.0.4
<current date 26.2> = 20201229:133233
27. Indices
27.1 Files
File Name |
Defined in |
AJH-GPIO-Relay.dts |
3.1 |
AdjustHeat.py |
16.1 |
BeagleClient.py |
3.5 |
BeagleDriver.py |
3.3 |
BeagleServer.py |
3.4 |
ChookDoor.py |
11.1 |
EventManager.py |
20.1 |
EventScheduler.py |
10.1 |
EventServer.py |
8.3 |
Events.py |
5.1 |
GardenSteps.py |
12.1 |
GardenWater.py |
13.1 |
HeatingModule.py |
14.16 |
HouseData.py |
19.2 |
HouseDefinitions.py |
2.2 |
HouseMade.py |
14.2 |
Makefile |
25.1 |
RelayControl.py |
4.17 |
RelayServer.py |
4.1 |
TempLog.py |
16.3 |
checkTime.py |
16.2 |
currentState.py |
19.1 |
eventEditor.py |
9.1, 9.2, 9.3
|
exportBeaglePins.sh |
3.2 |
house.py |
14.1 |
logsolar.py |
18.1 |
logwx.py |
15.2 |
solar.py |
18.2 |
startBeagleServer.sh |
23.1 |
startHouseData.sh |
19.5 |
startRelayServer.sh |
4.16 |
startTankLog.sh |
17.1 |
tank.py |
17.2 |
testRPC.py |
21.1 |
wx200.py |
15.1 |
27.2 Chunks
Chunk Name |
Defined in |
Used in |
ChookDoor: class ChookDoor |
11.3 |
11.1 |
ChookDoor: main |
11.15 |
11.1 |
ChookDoor: misc routines |
11.2 |
11.1 |
Event Manager: Clock |
20.3 |
20.1 |
Event Manager: Event class |
20.2 |
20.1 |
Event Manager: class eventManager |
20.5 |
20.1 |
Event Manager: get next event |
20.7 |
20.5 |
Event Manager: handle event |
20.8 |
20.5 |
Event Manager: main |
20.4 |
20.1 |
Event Manager: register event |
20.9 |
20.5 |
Event Manager: sort events |
20.6 |
20.5 |
Event Server: calling points |
8.5 |
8.3 |
Event Server: log message handling |
8.4 |
8.3 |
Event Server: main routine |
8.7 |
8.3 |
Event Server: serverprocess routine |
8.6 |
8.3 |
Event class: compare two events |
6.2 |
6.1 |
Event class: definition |
6.1 |
5.1 |
EventEditor: define get current events routine |
9.5 |
9.1 |
EventEditor: define make edit page routine |
9.7 |
9.1 |
EventEditor: define make home page routine |
9.6 |
9.1 |
EventEditor: print instructions |
9.4 |
9.1 |
EventList class: add event |
7.2 |
7.1 |
EventList class: definition |
7.1 |
5.1 |
EventList class: delete event |
7.3 |
7.1 |
EventList class: load events |
7.6 |
7.1 |
EventList class: nextEvent |
7.5 |
7.1 |
EventList class: save events |
7.7 |
7.1 |
EventList class: sort events |
7.4 |
7.1 |
EventServerRPCaddress |
8.2 |
2.3, 9.1, 10.1
|
EventServerRPCport |
8.1 |
8.2, 8.3
|
HouseData define getTemps |
19.3 |
19.2 |
HouseData define maxminTemp |
19.4 |
19.2 |
HouseDefinitions: general routines |
2.4 |
2.2 |
HouseDefinitions: server connections and interfaces |
2.3 |
2.2 |
HouseMade: Relay Control |
14.8 |
14.7 |
HouseMade: check client connection |
14.13 |
14.3 |
HouseMade: collect date and time data |
14.12 |
14.3, 14.16
|
HouseMade: define the Generate Solar Data routine |
14.10 |
14.2 |
HouseMade: define the Generate Tank Data section |
14.11 |
14.2 |
HouseMade: define the Generate Weather Data routine |
14.9 |
14.2 |
HouseMade: define the house interface |
14.3 |
14.2 |
HouseMade: generate the web page content |
14.14 |
14.3 |
HouseMade: get events information |
14.6 |
14.3 |
HouseMade: get local information |
14.4 |
14.3 |
HouseMade: get relay information |
14.5 |
14.3 |
HouseMade: legacy code for HouseMade.house |
14.7 |
14.3 |
Makefile: BeagleBone makes |
25.3 |
25.1 |
Makefile: RelayServer makes |
25.2 |
25.1 |
Makefile: install bastille |
25.4 |
|
RelayServer: connect to the BeagleServer |
4.2 |
4.1 |
Web: define the heatingData class |
14.17 |
14.16 |
Web: heating: build widths for web page table |
14.19 |
14.16 |
Web: heating: collect parameters and update |
14.18 |
14.16 |
check client IP address OK |
14.20 |
|
class ChookDoor: chookDoor |
11.10 |
11.3 |
class ChookDoor: closeDoor |
11.9 |
11.3 |
class ChookDoor: compute |
11.6 |
11.3 |
class ChookDoor: doorState |
11.11 |
11.3 |
class ChookDoor: handleEvent |
11.12 |
11.3 |
class ChookDoor: init |
11.4 |
11.3 |
class ChookDoor: load |
11.5 |
11.3 |
class ChookDoor: openDoor |
11.8 |
11.3 |
class ChookDoor: run |
11.13 |
11.3 |
class ChookDoor: save |
11.7 |
11.3 |
class ChookDoor: stop |
11.14 |
11.3 |
current date |
26.2 |
|
current version |
26.1 |
|
house make temperature panel |
14.15 |
|
relayserver: countDown |
4.15 |
4.1 |
relayserver: define getTank |
4.11 |
4.1 |
relayserver: define the RPC-Server interface |
4.4 |
4.1 |
relayserver: getSolar |
4.13 |
4.1 |
relayserver: getState |
4.5 |
4.1 |
relayserver: getTimer |
4.12 |
4.1 |
relayserver: readDoor |
4.6 |
4.1 |
relayserver: setBit |
4.8 |
4.1 |
relayserver: setBitOff |
4.10 |
4.1 |
relayserver: setBitOn |
4.9 |
4.1 |
relayserver: setState |
4.7 |
4.1 |
relayserver: start |
4.14 |
4.1 |
relayserver: strState |
4.3 |
4.1 |
27.3 Identifiers