HouseMade - The Hurst HouseHold Heater Helpmate

A.J.Hurst

Version 2.2.0

20201011:181914

Table of Contents

1 Introduction
1.1 Overview
1.2 TODOs
1.3 History
1.4 Philosophies
2 Key Data Structures
2.1 Edit Warning
2.2 House Definitions Module
2.2.1 HouseDefinitions: server connections and interfaces
2.2.2 HouseDefinitions: general routines
3 The BeagleBone System
3.1 The GPIO setup
3.2 the BeagleDriver.py program
3.3 The BeagleServer.py program
3.4 The BeagleClient.py program
4 The House Data Server and Relay Control System
4.1 Relay Server
4.1.1 RelayServer: connect to the BeagleServer
4.1.2 Define the convert State to String function
4.1.3 RelayServer: define the RPC-Server interface
4.1.4 relayserver: getState
4.1.5 relayserver: readDoor
4.1.6 relayserver: setState
4.1.7 relayserver: setBit
4.1.8 relayserver: setBitOn
4.1.9 relayserver: setBitOff
4.1.10 HouseData define getTank
4.1.11 relayserver: getTimer
4.1.12 relayserver: getSolar
4.1.13 relayserver: start
4.1.14 relayserver: countDown
4.2 Start the Relay Server
4.3 The Relay Controller Code
5 The Event Manager
5.1 The Event class
5.2 The Clock
5.3 Event Manager: main
5.4 The Event Manager class
5.4.1 the sortEvents method
5.4.2 the getNextEvent method
5.4.3 Handle an event
5.4.4 Register an Event
6 The Chook Door
6.1 ChookDoor: misc routines
6.2 ChookDoor: class ChookDoor
6.2.1 class ChookDoor: init
6.2.2 class ChookDoor: load
6.2.3 class ChookDoor: compute
6.2.4 class ChookDoor: save
6.2.5 class ChookDoor: Open Chook Door
6.2.6 class ChookDoor: Close Chook Door
6.2.7 class ChookDoor: chookDoor
6.2.8 class ChookDoor: doorState
6.2.9 class ChookDoor: handleEvent
6.2.10 class ChookDoor: run
6.2.11 class ChookDoor: stop
6.3 ChookDoor: main
7 The Garden Steps Lighting
8 The Garden Watering System
9 The Web Interface
9.1 The house.py cgi application
9.2 The HouseMade module
9.2.1 The house interface
9.2.2 Get Local Information
9.2.3 Get Relay Information
9.2.4 Legacy Code
9.2.5 The Relay Information section
9.2.6 define the Generate Weather Data routine
9.2.7 define the Generate Solar Data routine
9.2.8 define the Generate Tank Data routine
9.2.9 Collect Date and Time Data
9.2.10 Check the Client Connection
9.2.11 Generate the Web Page Content
9.2.12 Make the Temperature Panel (not currently used)
9.3 The HeatingModule module
9.3.1 Define the heatingData Class
9.3.2 Collect Parameters and Update
9.3.3 Build Widths for Web Page Table
10 The Weather System
10.1 The C Weather Monitor Program
10.2 The Python Interface to the Weather System
10.3 The Weather Logging Process
11 The Heating System
11.1 AdjustHeat
11.2 checkTime.py
11.3 TempLog
12 The Tank System
12.1 Water Tank Logging
12.2 Start the Tank Logging
12.3 Tank Module Functions
13 The Solar System
13.1 logsolar.py
13.2 solar.py
14 The House Computer
14.1 The Current State Interface
14.2 The HouseData Server (obsolete)
14.2.1 HouseData define getTemps
14.2.2 HouseData define maxminTemp
14.3 The startHouseData.sh script
15 Test Programs
15.1 Check RPC Operation
16 The Log Files
17 Installing and Starting the HouseMade Software
17.1 Introduction
17.2 Details
17.2.1 Start the Beagle Server
17.2.2 Relay Server
18 The Cron Jobs
19 The Makefile (separate file)
19.1 RelayServer Makes
19.2 BeagleBone Makes
19.3 Makefile: install bastille
20 Document History
21 Indices
21.1 Files
21.2 Chunks
21.3 Identifiers


1. Introduction

These programs have been distilled from Nathan Hurst's original Nautilus Shell suite of programs. The main differences are a) the change of name, b) the documentation, and c) consistency in logging structures (where possible).

1.1 Overview

There are a number of programs in this suite, and they are grouped into the following categories:

The Web Interface (operational)
Provides an easy to use interface to the program suite, using two key programs: house and timer (? check). These invoke the house computer.
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 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 House Computer (obsolete)
Provides logging of house data, as well as an RPC interface to access the logged data. This functionality has been subsumed by being incorporated into each subsystem, and a separate logging system is no longer used.
The Relay Control System (operational)
Uses an BeagleBone driving 8 relays to switch the various circuits.

1.2 TODOs

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:

  1. The tank logging has been migrated to a Python program, thus creating the potential for some more smarts in that subsection.
  2. 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.
  3. The chook door mechanism has been implemented, and is operational.
  4. The hostname had been hardcoded into the code, since I did not expect that it would be changing so rapidly. It has now been factored out, and replaced by the abstract title of central, a tribute to the original system which lasted for some 12 years.

1.4 Philosophies

The house computer complex is just that - complex. Some design principles are in order. One of the difficulties of design has been the need to maintain several different systems, for different reasons. The main systems are (in abstract terms): the data logger, the house controller, and the house web server. Currently the system is structured so that all of these functions are undertaken by the machine lilydale (see previous section History).

The first principle is then all data logging to be handled by the data logging system (as far as possible). Where this is not possible, the system responsible for collecting the data should transfer it to the logging system as soon as possible, and the logging system should be regarded as the definitive source for any data requests. While this principle was originally framed to permit it to be handled by a separate machine, it can reside anywhere on the house network.

The second principle is that any request to change the state of the house must be passed through the house controller, even if it is not the final direct control mechanism. The state of the house is defined by the state of the (currently) 12 relays controlled by the controller system. Again, there is no requirement that this functionality be co-located with the others.

The third principle is that all web traffic is handled by the web system. A key factor in deciding how this functionality is handled is whether the web server is secure enough (can external users change the heating or open the chook door, for example), and secondly whether it could handle the additional traffic imposed by an externally available web server.

2. Key Data Structures

2.1 Edit Warning

Provide a simple inclusion to flag that all derived files are extracted from this file, rather than stand-alone.

<edit warning 2.1> =
## ********************************************************** ## * do NOT EDIT THIS FILE! * ## * Use $HOME/Computers/House/HouseMade.xlp instead * ## **********************************************************
Chunk referenced in 10.1 11.1 11.2 11.3 12.1 12.2 13.1 13.2 14.2 14.5 15.1

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="newport.local" # 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='http://%s:8002' % (CENTRAL) # 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
Chunk referenced in 2.2

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/central/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
Chunk referenced in 2.2

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:

  1. Create and edit this dts file
  2. 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.)
  3. echo AJH-GPIO-Relay >$SLOTS
  4. Copy the dtbo file into /lib/firmware on the BeagleBone.
  5. 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 the BeagleDriver.py program

"BeagleDriver.py" 3.2 =
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.3 The BeagleServer.py program

"BeagleServer.py" 3.3 =
#!/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.2>

3.4 The BeagleClient.py program

"BeagleClient.py" 3.4 =
#!/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 House Data Server and Relay Control System

Currently, the laptop lilydale is used as both relay controller (through an Arduino circuit) and as general data logger and server for the various house functions. (An exception is the Chook Proving system, see section Chook Door Proving system.)

In this table, the relays are numbered left to right on the computer house panel. Bit 0 is the most significant (or leftmost) bit.

Relay Name Function Wire Colour
0
1
2
3 TopUp Top Up tanks from Mains
4 BottomVegBed Bottom Vegetable Bed Brown
5 MiddleVegBed Middle Vegetable Bed Green
6 TopVegBed Top Vegetable Bed Red
7 CarportVegBed Carport Vegetable Bed White
8 RainForest Rain Forest Sprayers White
9 Woo2Plas WooTank to PlasTank
10 FloodNDrain Flood And Drain
11 Heating Heating

Possibilities for the new relays:

4.1 Relay Server

The relay server runs all the time, offering a RPC interface to the relay driver. The relay state is represented as a n-element list, where each element represents the relay state as an integer 1 (relay on) or 0 (relay off). It can be controlled either by passing a full state list, or by turning individual bits on and off. The latter is to be preferred, to avoid parallel interaction conflicts.

"RelayServer.py" 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/central/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
Chunk referenced in 4.1

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
Chunk referenced in 4.1

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
Chunk referenced in 4.1

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')
Chunk referenced in 4.1

4.1.5 relayserver: readDoor

<relayserver: readDoor 4.6> =
def readDoor(): state=serverSend('read') return state server.register_function(readDoor, 'readDoor')
Chunk referenced in 4.1

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')
Chunk referenced in 4.1

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')
Chunk referenced in 4.1

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')
Chunk referenced in 4.1

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')
Chunk referenced in 4.1

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/central/tankState' p=open(statefile) l=p.readline() p.close() res=re.match('^(\d\d\d\d\d\d\d\d:\d\d\d\d\d\d) +(\d+) ',l) if res: level=int(res.group(2)) else: level=-1 # now temperature compensate the level, and calibrate litres=tank.convert(level) #logs.write("%s getTank()=>%s (from level=%s)\n" % (now,litres,level)) #logs.flush(); os.fsync(logs.fileno()) return litres server.register_function(getTank, 'getTank')
Chunk referenced in 4.1

4.1.11 relayserver: getTimer

<relayserver: getTimer 4.12> =
def getTimer(bitNo): remTime=currentTime[bitNo] return remTime server.register_function(getTimer, 'getTimer')
Chunk referenced in 4.1

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')
Chunk referenced in 4.1

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')
Chunk referenced in 4.1

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
Chunk referenced in 4.1

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 Start 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 =
LOGDIR='/home/ajh/logs/central' HOUSE='/home/ajh/Computers/House' BIN=${HOME}/bin if [ -f ${LOGDIR}/relayProcess ] ; then for p in `cat ${LOGDIR}/relayProcess` ; do kill -9 $p done fi rm ${LOGDIR}/relayProcess # 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.

"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 Event Manager

This is a new section in version 2, and represents 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:

  1. The event manager runs continuously, normally asleep, and once a minute wakes up to see whether an event is ready to be invoked.
  2. 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.
  3. 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).
  4. The handler may do various things, including re-registering itself with the event handler for a future event.

"EventManager.py" 5.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=GardenSteps.GardenSteps() gardenwater=GardenWater.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 5.2> <Event Manager: Clock 5.3> <Event Manager: class eventManager 5.5> <Event Manager: main 5.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.

5.1 The Event class

<Event Manager: Event class 5.2> =
class Event(): def __init__(self): self.event=None self.day='' self.time='00:00' self.operation='' self.parms='' self.handle=None
Chunk referenced in 5.1

5.2 The Clock

<Event Manager: Clock 5.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)
Chunk referenced in 5.1

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.

5.3 Event Manager: main

<Event Manager: main 5.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")
Chunk referenced in 5.1

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.

5.4 The Event Manager class

<Event Manager: class eventManager 5.5> =
class eventManager(): def __init__(self): self.fn=NEFname self.eventsList=[] self.isordered=False self.nextEvent=0 return <Event Manager: sort events 5.6> <Event Manager: get next event 5.7> <Event Manager: handle event 5.8> <Event Manager: register event 5.9>
Chunk referenced in 5.1

5.4.1 the sortEvents method

<Event Manager: sort events 5.6> =
def sortEvents(self): def sortkey(ev): return ev[1] evlist=self.eventsList evlist.sort(key=sortkey) self.isordered=True return evlist
Chunk referenced in 5.5

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.

5.4.2 the getNextEvent method

<Event Manager: get next event 5.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)
Chunk referenced in 5.5

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.

5.4.3 Handle an event

<Event Manager: handle event 5.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))
Chunk referenced in 5.5

5.4.4 Register an Event

<Event Manager: register event 5.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))
Chunk referenced in 5.5

6. 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.

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 EventManager module has the responsibility for driving the chook door events. Each module requiring action at a particular time needs to register with the event manager 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.

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" 6.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 6.2> <ChookDoor: class ChookDoor 6.3> <ChookDoor: main 6.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()

6.1 ChookDoor: misc routines

<ChookDoor: misc routines 6.2> =
def parse(pat,line): res=re.match(pat,line) if res: return res.group(1) else: return ''
Chunk referenced in 6.1

6.2 ChookDoor: class ChookDoor

<ChookDoor: class ChookDoor 6.3> =
class ChookDoor(): <class ChookDoor: init 6.4> <class ChookDoor: load 6.5> <class ChookDoor: compute 6.6> <class ChookDoor: save 6.7> <class ChookDoor: openDoor 6.8> <class ChookDoor: closeDoor 6.9> <class ChookDoor: chookDoor 6.10> <class ChookDoor: doorState 6.11> <class ChookDoor: handleEvent 6.12> <class ChookDoor: run 6.13> <class ChookDoor: stop 6.14>
Chunk referenced in 6.1

6.2.1 class ChookDoor: init

<class ChookDoor: init 6.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
Chunk referenced in 6.3

6.2.2 class ChookDoor: load

<class ChookDoor: load 6.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
Chunk referenced in 6.3

6.2.3 class ChookDoor: compute

<class ChookDoor: compute 6.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
Chunk referenced in 6.3

6.2.4 class ChookDoor: save

<class ChookDoor: save 6.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
Chunk referenced in 6.3

6.2.5 class ChookDoor: Open Chook Door

<class ChookDoor: openDoor 6.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")
Chunk referenced in 6.3

6.2.6 class ChookDoor: Close Chook Door

<class ChookDoor: closeDoor 6.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 opened")
Chunk referenced in 6.3

6.2.7 class ChookDoor: chookDoor

<class ChookDoor: chookDoor 6.10> =
def chookDoor(self,p): if str(p) in ['open','1']: self.openDoor() elif str(p) in ['close','shut','0']: self.closeDoor() else: raise(BadChook)
Chunk referenced in 6.3

6.2.8 class ChookDoor: doorState

<class ChookDoor: doorState 6.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)
Chunk referenced in 6.3

6.2.9 class ChookDoor: handleEvent

<class ChookDoor: handleEvent 6.12> =
def handleEvent(self,parms): try: self.chookDoor(parms) except: raise(BadChook)
Chunk referenced in 6.3

6.2.10 class ChookDoor: run

<class ChookDoor: run 6.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)
Chunk referenced in 6.3

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.

6.2.11 class ChookDoor: stop

<class ChookDoor: stop 6.14> =
def stop(self): # just print a message for now print("ChookDoor handler now terminating")
Chunk referenced in 6.3

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.

6.3 ChookDoor: main

<ChookDoor: main 6.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))
Chunk referenced in 6.1

7. The Garden Steps Lighting

This module follows the same model as the ChookDoor module, namely that itdefines a class GardenSteps has a run and a stop method. The run method registers the current events (turn lights on, turn lights off) with the EventManager, and the stop method is called to clean up when all events are done. The other methods included are fairly self explanatory.

"GardenSteps.py" 7.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,onoff): 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

8. The Garden Watering System

This module follows the same model as the GardenSteps module, namely that it defines a class GardenWater that has a run and a stop method. The run method registers the current events (turn sprinklers on, turn sprinklers off) with the EventManager, and the stop method is called to clean up when all events are done.

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.

"GardenWater.py" 8.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:10' 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

9. 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.

9.1 The house.py cgi application

"house.py" 9.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.

9.2 The HouseMade module

"HouseMade.py" 9.2 =
#!/usr/bin/python ## H o u s e M a d e . p y ## ## ********************************************************** ## * do NOT EDIT THIS FILE! * ## * Use $HOME/Computers/House/HouseMade.xlp instead * ## ********************************************************** import ChookDoor import datetime import cgi,math,string import re import time #import xmlrpclib #import currentState import HouseDefinitions from HouseDefinitions import * MServer='http://%s/~ajh/cgi-bin/house.py' % (CENTRAL) # Main (web) server <HouseMade: define the Generate Weather Data routine 9.8> <HouseMade: define the Generate Solar Data routine 9.9> <HouseMade: define the Generate Tank Data section 9.10> <HouseMade: define the house interface 9.3> if __name__=='__main__': house() ## ## The End ##

9.2.1 The house interface

<HouseMade: define the house interface 9.3> =
def house(remadr,server,args): import os,sys DEBUG=False <HouseMade: collect date and time data 9.11> <HouseMade: check client connection 9.12> # this is where to find any programs invoked in this module sys.path.append('/home/ajh/Computers/House/') ################################################## LOCAL INFORMATION ######### # Now get and display some local information. <HouseMade: get local information 9.4> # localinfo is a string containing local information ################################################## RELAY INFORMATION ######### # Now get and display the relay state information. # determine what relays are currently switched on <HouseMade: get relay information 9.5> # relayStateStr is a string containing the relay state information ################################################## OTHER INFORMATION ######### # # From here on is fairly irrelevant at the moment, and is here only # as legacy code. It will be tidied up in due course. <HouseMade: legacy code for HouseMade.house 9.6> # Currently no useful information returned # reload the page every second if a timer is active, otherwise only every minute if active: redirect="1;URL='%s'" % (MServer) else: redirect="60" housepage=<HouseMade: generate the web page content 9.13> return housepage
Chunk referenced in 9.2

9.2.2 Get Local Information

<HouseMade: get local information 9.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 = ''' <table> <tr> <td>%s sunrise is at </td><td style="color:#e04000"> %s </td> <td>and %s sunset is at </td><td style="color:#e04000"> %s </td> </tr> <tr> <td>The chook gate opening time is </td><td style="color:#c04000"> %s </td> <td> and the shutting time is </td><td style="color:#c04000"> %s </td> </tr> <tr> <td>The garden steps switch on at </td><td style="color:#c04000" align="right"> %s </td> <td> and then switch off at </td><td style="color:#c04000" align="right"> %s </td> </tr> <tr> <td>The garden watering turns on at </td><td style="color:#c04000" align="right"> %s </td> <td> and then turns off at </td><td style="color:#c04000" align="right"> %s </td> </tr> </table> ''' % (dayOpen,localrise,dayShut,localset,gateopentime,gateshuttime,stepsOn,stepsOff,waterOn,waterOff) nextEventFile=open(NEFname,'r') data=nextEventFile.readline() nextEventFile.close() res=re.match('(\d+) +(.*)$',data) if res: numEvs=int(res.group(1)) nextEv=res.group(2) if numEvs==0: localinfo += '<p>No more events for today</p>' else: localinfo += ''' <p> There are %s more scheduled events, and the next event is %s </p> ''' % (numEvs,nextEv) else: localinfo += 'No event data available'
Chunk referenced in 9.3

9.2.3 Get Relay Information

<HouseMade: get relay information 9.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)
Chunk referenced in 9.3

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.

9.2.4 Legacy Code

<HouseMade: legacy code for HouseMade.house 9.6> =
(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 9.7> ################### Water Storage tanksection='' # tank([]) ################### Solar Power solarsection='' # solar([]) ################### Climate weathersection='' # weather([]) ############################## ###################
Chunk referenced in 9.3

9.2.5 The Relay Information section

<HouseMade: Relay Control 9.7> =
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>'
Chunk referenced in 9.6

9.2.6 define the Generate Weather Data routine

<HouseMade: define the Generate Weather Data routine 9.8> =
def weather(args): WServer='http://%s:5000/weather' % (CENTRAL) MServer='http://%s:5000/house' % (CENTRAL) MAXMINFILE='/home/ajh/logs/central/maxmintemps.log' w=wx200.weather() curtemp = w.inside.temp curhumid = w.inside.humidity outtemp = w.outside.temp outhumid = w.outside.humidity starttime=datetime.datetime.now() yesterday=starttime-datetime.timedelta(days=1) yesterday=yesterday.strftime("%Y%m%d") today=starttime.strftime("%Y%m%d") # check maximum and minimum maxminpat='(\d\d\d\d\d\d\d\d)' # date only maxminpat+=' +([0-9.]+)' # maximum temp maxminpat+=' +([0-9:]+)' # maximum temp time maxminpat+=' +([0-9.]+)' # minimum temp maxminpat+=' +([0-9:]+)' # minimum temp time maxminpat=re.compile(maxminpat) f=open(MAXMINFILE,'r') maxmintable={} for l in f.readlines(): #print("read maxmin line of %s" % (l)) res=maxminpat.match(l) if res: d=res.group(1) max=float(res.group(2)) maxat=res.group(3) min=float(res.group(4)) minat=res.group(5) maxmintable[d]=(max,maxat,min,minat) else: print("cannot parse %s" % (l)) f.close() if maxmintable.has_key(yesterday): (yestermax,yestermaxat,yestermin,yesterminat)=maxmintable[yesterday] else: print("<P>Min/Max temperatures not available for yesterday</P>\n") (yestermax,yestermaxat,yestermin,yesterminat)=(0.0, '00:00', 0.0, '00:00') if maxmintable.has_key(today): (max,maxat,min,minat)=maxmintable[today] else: print("<P>Min/Max temperatures not available for today</P>\n") (max,maxat,min,minat)=(0.0, '00:00', 0.0, '00:00') # get desired temperature house=currentState.HouseState() house.load() aimtemp=house.get('thermostat') onoffcolor="black" 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
Chunk referenced in 9.2

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.

9.2.7 define the Generate Solar Data routine

<HouseMade: define the Generate Solar Data routine 9.9> =
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
Chunk referenced in 9.2

Collect the solar power information for display and generate the related text.

9.2.8 define the Generate Tank Data routine

<HouseMade: define the Generate Tank Data section 9.10> =
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
Chunk referenced in 9.2

This section now installed on lilydale.

9.2.9 Collect Date and Time Data

<HouseMade: collect date and time data 9.11> =
# 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
Chunk referenced in 9.3 9.15

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.

9.2.10 Check the Client Connection

<HouseMade: check client connection 9.12> =
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>")
Chunk referenced in 9.3

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.

9.2.11 Generate the Web Page Content

<HouseMade: generate the web page content 9.13> =
""" <HTML> <HEAD> <LINK REL="SHORTCUT ICON" HREF="favicon.ico"> <meta http-equiv="Refresh" content="%(redirect)s"> <meta http-equiv="Pragma" content="no-cache"> <TITLE>HouseMade</TITLE> </HEAD> <BODY> <h1> <a href="(MServer)s"> HouseMade: the Hurst House Heater Helpmate </a> </h1> HouseMade thinks it is currently %(tm)s. You might want to see what rain is <a href="http://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>. %(localinfo)s %(relayStateStr)s %(relaycontrol)s %(weathersection)s %(solarsection)s %(tanksection)s </BODY> </HTML> """ % vars()
Chunk referenced in 9.3

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.

9.2.12 Make the Temperature Panel (not currently used)

<house make temperature panel 9.14> =
# 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.

9.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" 9.15 = **** 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 9.16> def heating(logMsg,remadr,args): DEBUG=False active=False <HouseMade: collect date and time data 9.11> 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 9.17> <Web: heating: build widths for web page table 9.18> # 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()

9.3.1 Define the heatingData Class

<Web: define the heatingData class 9.16> =
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 9.15

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.

9.3.2 Collect Parameters and Update

<Web: heating: collect parameters and update 9.17> =
# 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 9.15

9.3.3 Build Widths for Web Page Table

<Web: heating: build widths for web page table 9.18> =
# 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 9.15
<check client IP address OK 9.19> =
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.

10. 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.

10.1 The C Weather Monitor Program

(Details to be recorded)

10.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" 10.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)))

10.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" 10.2 = **** File not generated!
import datetime import re from wx200 import * STATEFILE='/home/ajh/logs/central/wxState.txt' MAXMINFILE='/home/ajh/logs/central/maxmintemps.log' w=weather() inside=w.inside intemp=inside.temp outside=w.outside wind=w.wind rain=w.rain now=datetime.datetime.now() nowstamp=now.strftime("%Y%m%d:%H%M%S") if intemp==0.0: f=open(STATEFILE,'r') prev=f.readline() res=re.match('\d{8}:\d{6} +(\d+\.\d) +(\d+\.\d)',prev) if res: intemp=float(res.group(1)) outside.temp=float(res.group(2)) else: print("Could not match %s" % (prev)) line="%s %5.1f %5.1f" % (nowstamp,intemp,outside.temp) line+=" %5.1f %5.1f" % (inside.humidity,outside.humidity) line+=" %5.1f" % (outside.dew) line+=" %5.1f %5.1f" % (wind.gust,wind.gustdir) line+=" %5.1f %5.1f" % (rain.rate,rain.daily) print(line) # save current state f=open(STATEFILE,'w') f.write(line+'\n') f.close() # check maximum and minimum maxminpat='(\d\d\d\d\d\d\d\d)' # date only maxminpat+=' +([0-9.]+)' # maximum temp maxminpat+=' +([0-9:]+)' # maximum temp time maxminpat+=' +([0-9.]+)' # minimum temp maxminpat+=' +([0-9:]+)' # minimum temp time maxminpat=re.compile(maxminpat) f=open(MAXMINFILE,'r') maxmintable={} for l in f.readlines(): #print("read maxmin line of %s" % (l)) res=maxminpat.match(l) if res: d=res.group(1) max=float(res.group(2)) maxat=res.group(3) min=float(res.group(4)) minat=res.group(5) maxmintable[d]=(max,maxat,min,minat) else: print("cannot parse %s" % (l)) f.close() changed=False today=now.strftime("%Y%m%d") time=now.strftime("%H:%M") if 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()

11. The Heating System

11.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" 11.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.

11.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" 11.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.

11.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" 11.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)

12. The Tank System

Define here those program components concerned (solely) with recording and suppling water tank information.

12.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

12.2 Start the Tank Logging

"startTankLog.sh" 12.1 = **** File not generated!
<edit warning 2.1> LOGDIR='/home/ajh/logs/central' HOUSE='/home/ajh/Computers/House' USB=`getDevice tank` kill -9 `cat ${LOGDIR}/tankProcess` rm ${LOGDIR}/tankProcess python tank.py $USB & ps aux | grep "python tank.py" | grep -v grep | awk '{print $2}' \ >>${LOGDIR}/tankProcess

The tank logging is performed slightly differently from the other logging operations, as the tank level transducer operates in a free-running open loop mode. Approximately once a second it sends a burst of data down its RS232 connection, and so it is necessary to have the logging program running constantly to hear those data bursts. This is the purpose of the tank.py program.

The rest of this code is concerned with logging the process ID of the logging program itself, so that it can be started and stopped reliably, without interference from any previous instance.

12.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" 12.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/central/tank.log" maxlitres = 2250 # for one tank minlitres = 0 # actual somewhat more maxcap = 68750 # capacitance reading for full supply level mincap = 11484 # capacitance reading when tank is empty NumberOfTanks=2 if NumberOfTanks>1: maxlitres *= NumberOfTanks minlitres *= NumberOfTanks slope=(maxlitres-minlitres)/float(maxcap-mincap) base=minlitres-slope*mincap def compensate(level,temp,ntanks=NumberOfTanks): ''' returns a temperature compensated tank level (NOT litres)''' level=level+213*(temp-28.2)*(level/73035.0) return level def convert(level): '''returns the (uncompensated) volume corresponding to the capacitance meter output value @level. ''' litres=base+slope*float(level) return litres def read_tank_state(): f=open('/home/ajh/logs/central/tankState','r') l=f.readline() if len(l) <= 1: return l = string.split(l) if len(l) < 3: return (level, temp) = map(int, l[1:3]); return (level, temp) def calibrated(): tankdepth,tanktemp=read_tank_state() bigtankheight=1450 tankdepth = (float(tankdepth-mincap)/float(maxcap-mincap))*bigtankheight volume = compensate(tankdepth,tanktemp) return (volume,tanktemp) def readraw(dev): device=os.open(dev,os.O_RDWR) res='' while not res: res=os.read(device,14) time.sleep(5) os.close(device) return res def main(): (level,temp)=read_tank_state() (volume, temp) = calibrated() format = "capacitance reading = %f, " format += "tank temperature = %f" print(format % (level, temp*0.1)) uncompensated=convert(level) print("uncompensated volume = %5.1f" % (uncompensated)) if __name__ == '__main__': (vals,path)=getopt.getopt(sys.argv[1:],'',[]) usb=usbFind.USBclass() tankDevice=usb.device2port('tank') lastMin=60 while True: rawtank=readraw(tankDevice) res=re.match('^ *(\d+) +(\d+) +(\d+).*$',rawtank) if res: level=int(res.group(1)) volts=int(res.group(3)) now=datetime.datetime.now() thisMin=now.minute now=now.strftime("%Y%m%d:%H%M%S") if thisMin!=lastMin: logfile=open(LOGFILENAME,'a') logfile.write("%s %d %d\n" % (now,level,volts)) logfile.close() #print("%s %d %d" % (now,level,volts)) lastMin=thisMin pass # end if pass # end while

The temperature compensation values were worked out from a pair of observations:

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.

13. 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.

13.1 logsolar.py

"logsolar.py" 13.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.

13.2 solar.py

Provide definitions and access functions for the solar controller.

"solar.py" 13.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.

14. The House Computer

14.1 The Current State Interface

"currentState.py" 14.1 =
#!/usr/bin/python Debug=False STATEFILE='/home/ajh/logs/central/houseState' class HouseState(): def __init__(self): state={} def load(self,fname=STATEFILE): f=open(fname,'r') statedata=f.read() if Debug: print(statedata) self.state=eval(statedata) f.close() def get(self,name): if self.state.has_key(name): return self.state[name] else: print("invalid name %s" % (name)) return None def store(self,name,value): self.state[name]=value def save(self,fname=STATEFILE): fn=fname if Debug: fn+='2' f=open(fn,'w') f.write('{\n') for k in self.state.keys(): if Debug: print("saving %s:%s" % (k,self.state[k])) value=self.state[k] if isinstance(value,str): f.write(" '%s':'%s',\n" % (k,value)) else: f.write(" '%s':%s,\n" % (k,value)) f.write('}\n') f.close() def main(): house=HouseState() house.load() if Debug: print(house.get('test')) house.save() if __name__=='__main__': main()

The currentState routine provides an easy access mechanism to get the current state of various house values, as computed by the various routines. This is in the form of a module that is to be imported by any routine changing the value of a key variable, and provides persistent storage of that variable, until the next time it may be updated.

14.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" 14.2 =
#!/usr/bin/python <edit warning 2.1> import datetime import re import subprocess import tank import solar from SimpleXMLRPCServer import SimpleXMLRPCServer from SimpleXMLRPCServer import SimpleXMLRPCRequestHandler # Restrict to a particular path. class RequestHandler(SimpleXMLRPCRequestHandler): rpc_paths = ('/RPC2',) # Create server server = SimpleXMLRPCServer(("10.0.0.101", 8001), requestHandler=RequestHandler) server.register_introspection_functions() # define some crucial patterns # first, for parsing the temperature (weather station) log file: temppat='(\d\d\d\d\d\d\d\d:\d\d\d\d\d\d)' # date and time temppat+=' +([0-9.]+)' # inside temp temppat+=' +([0-9.]+)' # outside temp temppat+=' +([0-9.]+)' # inside humidity temppat+=' +([0-9.]+)' # outside humidity temppat+=' +([0-9.]+)' # outside dew point temppat+=' +([0-9.]+)' # heating thermostat temppat+=' +(.+)$' # heating on/off temppat=re.compile(temppat) maxminpat='(\d\d\d\d\d\d\d\d)' # date only maxminpat+=' +([0-9.]+)' # maximum temp maxminpat+=' +([0-9:]+)' # maximum temp time maxminpat+=' +([0-9.]+)' # minimum temp maxminpat+=' +([0-9:]+)' # minimum temp time maxminpat=re.compile(maxminpat) # Define and Register the getHeating function def getHeating(): p=open('/logdisk/logs/heating','r') temp=float(p.readline()) onoff=p.readline().strip() p.close() return (temp,onoff) server.register_function(getHeating, 'getHeating') # Define and Register the setHeating function def set(temp,onoff): p=open('/logdisk/logs/heating','w') p.write("%5.2f\n%s\n" % (temp,onoff)) p.close() return 'OK' server.register_function(set, 'setHeating') # Define and Register the getSolar function # moved to RelayServer, 20150509:113400 <HouseData define getTemps 14.3> server.register_function(getTemps, 'getTemps') <HouseData define maxminTemp 14.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)).

14.2.1 HouseData define getTemps

<HouseData define getTemps 14.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)
Chunk referenced in 14.2

14.2.2 HouseData define maxminTemp

<HouseData define maxminTemp 14.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
Chunk referenced in 14.2

14.3 The startHouseData.sh script

"startHouseData.sh" 14.5 =
#!/bin/bash <edit warning 2.1> LOGDIR=/logdisk/logs HOUSEPROC=$LOGDIR/houseProcess HOUSEDIR=/home/ajh/Computers/House kill -9 `cat $HOUSEPROC` /usr/bin/python $HOUSEDIR/HouseData.py >> $LOGDIR/housedata.log 2>&1 & ps aux | grep "HouseData.py" | grep -v grep | awk '{print $2}' >$HOUSEPROC

15. Test Programs

15.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" 15.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.

16. 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

17. Installing and Starting the HouseMade Software

17.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:

  1. 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.
  2. The House Computer (currently newport, but soon to be migrated to ouyen) relay interface RelayServer.py.
  3. chook door (the need for this as a standalone is currently in question).

17.2 Details

17.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" 17.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).

17.2.2 Relay Server

The RelayServer runs continously on the House computer (aka Central, currently set as newport, but about to be changed to ouyen. 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 newport/ouyen /home/ajh/Computers/House/startRelayCentral.sh
          
Since this program runs continuously, it should be started in a separate terminal window, which is then hidden from view, but left running.

18. The Cron Jobs

Currently no cron jobs are required.

19. The Makefile (separate file)

"Makefile" 1.1 =
default=HouseMade GenFiles = house.py HouseData.py RelayServer.py CGI = ${HOME}/www/cgi-bin HOUSE = ${HOME}/Computers/House BIN = ${HOME}/bin CENTRAL = newport.local CENTRALHOUSE = $(CENTRAL):$(HOME)/Computers/House NEWPORT = newport.local NEWPORTHOUSE = $(NEWPORT):$(HOME)/Computers/House OUYEN = ouyen.local OUYENHOUSE = $(OUYEN):$(HOME)/Computers/House KERANG = kerang KERANGHOUSE = $(KERANG):$(HOME)/Computers/House XSLLIB = /home/ajh/lib/xsl XSLFILES = $(XSLLIB)/lit2html.xsl $(XSLLIB)/tables2html.xsl AUTEUIL = auteuil:${HOME}/Computers/House BASTILLE = bastille:${HOME}/Computers/House LILYDALE = lilydale:${HOME}/Computers/House WEBSOURCE = ${HOME}/www/computing/sources/house SCP = /usr/bin/scp include ${HOME}/etc/MakeXLP ${GenFiles}:HouseMade.tangle #install: install-flask install-definitions install-house \ # install-timer install-startFlask install: install-central HouseMade.tangle HouseMade.xml: HouseMade.xlp Makefile.xlp xsltproc --xinclude $(XSLLIB)/litprog.xsl HouseMade.xlp >HouseMade.xml touch HouseMade.tangle RelayControl.tangle Makefile.tangle HouseMade.html: HouseMade.xml $(XSLFILES) xsltproc --xinclude $(XSLLIB)/lit2html.xsl HouseMade.xml >HouseMade.html html: HouseMade.html pdf: HouseMade.pdf ################################### # These are the NEW install makes # ################################### CGIFILES=house.py HouseMade.py ChookDoor.py ChookDoor.py RelayServer.py make-executable: HouseMade.tangle chmod 755 house.py RelayServer.py RelayControl.py chmod 755 startRelayServer.sh chmod 755 EventManager.py # this file not needed in the cgi-bin # install-central installs all source files into the House directory # on CENTRAL, whatever that it install-central: make-executable @if [ $(CENTRAL) = $(NEWPORT) ] ; then \ rsync -auv $(CGIFILES) $(NEWPORT):/home/ajh/public_html/cgi-bin/ ; \ fi @if [ $(CENTRAL) = $(OUYEN) ] ; then \ rsync -auv $(CGIFILES) $(OUYEN):/home/ajh/public_html/cgi-bin/ ; \ fi <Makefile: RelayServer makes 1.2> <Makefile: BeagleBone makes 1.3> ############################## # old makes, keep for legacy # ############################## # install flask modules # keep these arranged alphabetically install-AdjustHeat: HouseMade.tangle $SCP AdjustHeat.py ${KERANG}/ touch install-AdjustHeat install-arduino: RelayControl.tangle $SCP getArduinoUSB.sh kerang:/home/ajh/bin/ $SCP startArduino.sh ${KERANG}/ $SCP setupMake.sh ${KERANG}/ touch install-arduino install-chook: HouseMade.tangle $SCP ChookProve.py ${AUTEUIL}/ $SCP ChookDoorServer.py ${AUTEUIL}/ install-definitions: HouseMade.tangle $SCP -q HouseDefinitions.py ${KERANG}/ touch install-definitions install-everyMinute: HouseMade.tangle chmod 755 EveryMinute.sh $SCP -q EveryMinute.sh ${KERANG}/ touch install-everyMinute install-fullflask: install-flask install-house install-timer install-definitions install-watchFlask install-flask: HouseMade.tangle chmod 755 kerangFlask.py $SCP -q kerangFlask.py ${KERANG}/ touch install-flask install-html: HouseMade.html if /usr/bin/diff HouseMade.html ${WEBSOURCE}/HouseMade.html; then \ cp HouseMade.html ${WEBSOUCE}/HouseMade.html ;\ fi install-startFlask: HouseMade.tangle chmod 755 startFlask.sh $SCP -q startFlask.sh ${KERANG}/ touch install-startFlask install-tank: HouseMade.tangle $SCP tank.py ${KERANG}/ $SCP tankplot.py ${KERANG}/ $SCP startTankLog.sh ${KERANG}/ touch install-tank install-timer: HouseMade.tangle $SCP -q TimerModule.py ${KERANG}/ touch install-timer install-watchFlask: HouseMade.tangle $SCP -q watchFlask.py ${KERANG}/../../bin/ echo "Check sudo sticky bit on watchFlask.py" ssh -q kerang ls -l ${KERANG}/../../bin/watchFlask.py touch install-watchFlask start-auteuil: install-chook ssh root@auteuil chmod 4711 ${HOUSE}/ChookProve.py ssh auteuil /usr/bin/python ${HOUSE}/ChookProve.py ssh auteuil /usr/bin/python ${HOUSE}/ChookDoorServer.py start-flask: install-startFlask install-flask ssh kerang ${HOUSE}/startFlask.sh touch start-flask start-arduino: install-arduino ssh kerang ${HOUSE}/startArduino.sh touch start-arduino start-tank: install-tank ssh kerang ${HOUSE}/startTankLog.sh touch start-tank <Makefile: install bastille 1.4> **** Chunk omitted! ########################### # MAKEFILE # ########################### makefile: Makefile.tangle ###################### # INSTALL BY PROGRAM # ###################### install-tank.py: HouseMade.tangle rsync -auv tank.py ${HOME}/lib/python/ rsync -auv tank.py garedelyon:/home/ajh/lib/python/ rsync -auv tank.py flinders:/home/ajh/lib/python/ rsync -auv tank.py flinders:/home/ajh/Computers/House/ ############################# # END OF INSTALLATION STUFF # ############################# executable: house.py timer.py chmod 755 house.py timer.py all: HouseMade.html clean: litclean -rm $(GenFiles)

19.1 RelayServer Makes

<Makefile: RelayServer makes 1.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
Chunk referenced in 1.1

19.2 BeagleBone Makes

<Makefile: BeagleBone makes 1.3> =
install-beagle: HouseMade.tangle chmod 755 BeagleServer.py chmod 755 startBeagleServer.sh $(SCP) BeagleServer.py BeagleDriver.py $(KERANGHOUSE)/ $(SCP) BeagleClient.py startBeagleServer.sh $(KERANGHOUSE)/ ssh $(KERANG) mv $(HOUSE)/startBeagleServer.sh $(HOME)/bin/ touch install-beagle start-beagle: HouseMade.tangle install-beagle ssh kerang $(HOME)/bin/startBeagleServer.sh touch start-beagle
Chunk referenced in 1.1

19.3 Makefile: install bastille

<Makefile: install bastille 1.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-definitions: HouseMade.tangle $SCP HouseDefinitions.py ${BASTILLE}/ touch install-definitions install-everyMinute: HouseMade.tangle chmod 755 EveryMinute.sh $SCP EveryMinute.sh ${BASTILLE}/ touch install-everyMinute install-flask: HouseMade.tangle install-house install-timer install-definitions chmod 755 bastilleFlask.py $SCP bastilleFlask.py ${BASTILLE}/ touch install-flask install-house: HouseMade.tangle install-definitions $SCP HouseMade.py ${BASTILLE}/ touch install-house install-html: HouseMade.html if /usr/bin/diff HouseMade.html ${WEBSOURCE}/HouseMade.html; then \ cp HouseMade.html ${WEBSOUCE}/HouseMade.html ;\ fi install-startFlask: HouseMade.tangle chmod 755 startFlask.sh $SCP startFlask.sh ${BASTILLE}/ touch install-startFlask install-startRelay: HouseMade.tangle install-relay chmod 755 startRelayServer.sh $SCP startRelayServer.sh $(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-bastille: start-flask start-relay start-arduino touch start-bastille start-flask: install-startFlask install-flask ssh bastille ${HOUSE}/startFlask.sh touch start-flask start-relay: install-startRelay ssh bastille ${HOUSE}/startRelayServer.sh touch start-relay start-arduino: install-arduino ssh bastille ${HOUSE}/startArduino.sh touch start-arduino start-tank: install-tank ssh bastille ${HOUSE}/startTankLog.sh touch start-tank
Chunk referenced in 1.1

20. 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
<current version 19.1> = 2.2.0
<current date 19.2> = 20201011:181914

21. Indices

21.1 Files

File Name Defined in
AJH-GPIO-Relay.dts 3.1
AdjustHeat.py 11.1
BeagleClient.py 3.4
BeagleDriver.py 3.2
BeagleServer.py 3.3
ChookDoor.py 6.1
EventManager.py 5.1
GardenSteps.py 7.1
GardenWater.py 8.1
HeatingModule.py 9.15
HouseData.py 14.2
HouseDefinitions.py 2.2
HouseMade.py 9.2
Makefile 1.1
RelayControl.py 4.17
RelayServer.py 4.1
TempLog.py 11.3
checkTime.py 11.2
currentState.py 14.1
house.py 9.1
logsolar.py 13.1
logwx.py 10.2
solar.py 13.2
startBeagleServer.sh 17.1
startHouseData.sh 14.5
startRelayServer.sh 4.16
startTankLog.sh 12.1
tank.py 12.2
testRPC.py 15.1
wx200.py 10.1

21.2 Chunks

Chunk Name Defined in Used in
ChookDoor: class ChookDoor 6.3 6.1
ChookDoor: main 6.15 6.1
ChookDoor: misc routines 6.2 6.1
Event Manager: Clock 5.3 5.1
Event Manager: Event class 5.2 5.1
Event Manager: class eventManager 5.5 5.1
Event Manager: get next event 5.7 5.5
Event Manager: handle event 5.8 5.5
Event Manager: main 5.4 5.1
Event Manager: register event 5.9 5.5
Event Manager: sort events 5.6 5.5
HouseData define getTemps 14.3 14.2
HouseData define maxminTemp 14.4 14.2
HouseDefinitions: general routines 2.4 2.2
HouseDefinitions: server connections and interfaces 2.3 2.2
HouseMade: Relay Control 9.7 9.6
HouseMade: check client connection 9.12 9.3
HouseMade: collect date and time data 9.11 9.3, 9.15
HouseMade: define the Generate Solar Data routine 9.9 9.2
HouseMade: define the Generate Tank Data section 9.10 9.2
HouseMade: define the Generate Weather Data routine 9.8 9.2
HouseMade: define the house interface 9.3 9.2
HouseMade: generate the web page content 9.13 9.3
HouseMade: get local information 9.4 9.3
HouseMade: get relay information 9.5 9.3
HouseMade: legacy code for HouseMade.house 9.6 9.3
Makefile: BeagleBone makes 1.3 1.1
Makefile: RelayServer makes 1.2 1.1
Makefile: install bastille 1.4 1.1
RelayServer: connect to the BeagleServer 4.2 4.1
Web: define the heatingData class 9.16 9.15
Web: heating: build widths for web page table 9.18 9.15
Web: heating: collect parameters and update 9.17 9.15
check client IP address OK 9.19
class ChookDoor: chookDoor 6.10 6.3
class ChookDoor: closeDoor 6.9 6.3
class ChookDoor: compute 6.6 6.3
class ChookDoor: doorState 6.11 6.3
class ChookDoor: handleEvent 6.12 6.3
class ChookDoor: init 6.4 6.3
class ChookDoor: load 6.5 6.3
class ChookDoor: openDoor 6.8 6.3
class ChookDoor: run 6.13 6.3
class ChookDoor: save 6.7 6.3
class ChookDoor: stop 6.14 6.3
current date 19.2
current version 19.1
house make temperature panel 9.14
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

21.3 Identifiers

Identifier Defined in Used in
Clock 5.3 5.4
fastmode 5.4 5.4, 5.4
heating 9.15
house 9.3
hysteresis 11.1
jobtime 9.11
testing 5.4 5.4

31 accesses since 29 Jan 2019, HTML cache rendered at 20201018:1347