HouseMade - The Hurst HouseHold Heater Helpmate


Version 3.0.4


The EventServer version has now been merged with this code.

Table of Contents

1 Introduction
1.1 Overview
1.2 TODOs
1.3 History
1.4 Progress since 2015
1.5 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 Restarting the BeagleBone Server
3.2.1 Restore the Pin Definitions
3.2.2 Restart the BeagleBone Server proper
3.3 the program
3.4 The program
3.5 The program
4 The Relay Server
4.1 Relay Server Code
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 Starting the Relay Server
4.3 The Relay Controller Code
5 The Events Module
6 The Event Class
7 The EventList class
8 The Event Server
8.1 Event Server Log Message Handling
8.2 Event Server calling points
8.3 Event Server: serverprocess routine
8.4 Event Server: main routine
9 The Event Editor
9.1 The EventEditor Instructions
9.2 EventEditor: get current events routine
9.3 EventEditor: Make Home Page
9.4 EventEditor: Make Edit Page
10 The Event Scheduler
11 The Chook Door
11.1 ChookDoor: misc routines
11.2 ChookDoor: class ChookDoor
11.2.1 class ChookDoor: init
11.2.2 class ChookDoor: load
11.2.3 class ChookDoor: compute
11.2.4 class ChookDoor: save
11.2.5 class ChookDoor: Open Chook Door
11.2.6 class ChookDoor: Close Chook Door
11.2.7 class ChookDoor: chookDoor
11.2.8 class ChookDoor: doorState
11.2.9 class ChookDoor: handleEvent
11.2.10 class ChookDoor: run
11.2.11 class ChookDoor: stop
11.3 ChookDoor: main
12 The Garden Steps Lighting
13 The Garden Watering System
14 The Web Interface
14.1 The cgi application
14.2 The HouseMade module
14.2.1 The house interface
14.2.2 Get Local Information
14.2.3 Get Relay Information
14.2.4 Get Events Information
14.2.5 Legacy Code
14.2.6 The Relay Information section
14.2.7 define the Generate Weather Data routine
14.2.8 define the Generate Solar Data routine
14.2.9 define the Generate Tank Data routine
14.2.10 Collect Date and Time Data
14.2.11 Check the Client Connection
14.2.12 Generate the Web Page Content
14.2.13 Make the Temperature Panel (not currently used)
14.3 The HeatingModule module
14.3.1 Define the heatingData Class
14.3.2 Collect Parameters and Update
14.3.3 Build Widths for Web Page Table
15 The Weather System
15.1 The C Weather Monitor Program
15.2 The Python Interface to the Weather System
15.3 The Weather Logging Process
16 The Heating System
16.1 AdjustHeat
16.3 TempLog
17 The Tank System
17.1 Water Tank Logging
17.2 Start the Tank Logging
17.3 Tank Module Functions
18 The Solar System
19 The House Computer
19.1 The Current State Interface
19.2 The HouseData Server (obsolete)
19.2.1 HouseData define getTemps
19.2.2 HouseData define maxminTemp
19.3 The script
20 The Event Manager
20.1 The Event class
20.2 The Clock
20.3 Event Manager: main
20.4 The Event Manager class
20.4.1 the sortEvents method
20.4.2 the getNextEvent method
20.4.3 Handle an event
20.4.4 Register an Event
21 Test Programs
21.1 Check RPC Operation
22 The Log Files
23 Installing and Starting the HouseMade Software
23.1 Introduction
23.2 Details
23.2.1 Start the Beagle Server
23.2.2 Relay Server
24 The Cron Jobs
25 Makefile
25.1 RelayServer Makes
25.2 BeagleBone Makes
25.3 Makefile: install bastille
26 Document History
27 Indices
27.1 Files
27.2 Chunks
27.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 Beagle Bone System (operational:
The hardware interface to the relay drivers.
The Relay Server (operational: CENTRAL:8001)
Provides an RPC interface to controlling the house relays.
The Event Server (operational: <EventServerRPCaddress 8.2>)
Provides an RPC interface to controlling the various house events.
The Web Interface (operational)
Provides an easy to use interface to the program suite, using two key programs: house and timer (? check). These invoke the house computer.
The Chook Door System (operational)
Controls the opening and shutting of the Chook House Door.
The Garden Steps System (operational)
Controls the switching of the garden steps lights.
The Garden Watering System (operational)
Controls the watering of various garden irrigation outlets.
The Relay Control System (operational)
Uses an BeagleBone driving 8 relays to switch the various circuits.
The Heating System (to be reinstated)
This subsystem controls the house heating system. Both the temperature and timing may be controlled: the temperature in steps of 1 degree Celsius, form 10 to 26 degrees, and the time in steps of 5 minutes from midnight to midnight, 7 (distinct) days of the week. Up to 7 time blocks per day are permitted.
The Tank System (to be reinstated)
Provides monitoring of the water storage facilities.
The Solar System (to be reinstated)
Provides monitoring of the solar photovoltaic systems, together with the inverter and UPS sub-systems.
The House Computer (obsolete)
Provides logging of house data, as well as an RPC interface to access the logged data. This functionality has been subsumed by being incorporated into each subsystem, and a separate logging system is no longer used.

1.2 TODOs

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 Progress since 2015

In Oct 2016 we moved house, since we had embarked upon a complete renovation of the existing house, and we needed to vacate the premises. All the house automation system had to be dismantled, since every room in the house was undergoing some sort of renovation, and the cellar was no exception. For the remainder of 2016, all of 2017, and the first few months of 2018, the HouseMade system was out of action.

Progress was slow in reinstating the system, partly because of the disruption to the cellar, and partly because most of the components were in storage. The cellar had been dug out to expand the space available, and most of our spare time in 2019 was spent in making it more habitable. A new floor went in, a baseboard for the new model railway constructed (see History), drawers and shelves installed and a new (secondhand) rack to house the house server and central replacement brought in. It made a big difference to the space! This was largely complete by mid 2020, by which time the chook door opening mechanism had been reinstated, and recabled.

A complete redesign of the system was undertaken, reusing some components from the previous design, but now based upon an event-driven server-based model. This is described below in the relevant sections. The new hardware consists of a BeagleBone Black (the relay driving processor), which talks to a decentralized Relay Server controlling the relay driver, and a decentralized Event Server, providing generalized event services to web pages and the like. These servers have been designed and tested on my desktop, but are slowly migrating to the house server (a large rack-based disk farm built from a variety of components). In addition, a Raspberry Pi Model 4 is being developed to handle data collection and logging.

As of Dec 2020, the new system is operational, driving the chook door, garden lights (a new addition to the controlled enviroment), and the garden irrigation system. Further development to reinstate the heating system and data logging systems is underway.

1.5 Philosophies

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

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

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

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

2. Key Data Structures

2.1 Edit Warning

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

<edit warning 2.1> =
## ********************************************************** ## * do NOT EDIT THIS FILE! * ## * Use $HOME/Computers/House/HouseMade.xlp instead * ## **********************************************************
Chunk referenced in 15.1 16.1 16.2 16.3 17.1 17.2 18.1 18.2 19.2 19.5 21.1

2.2 House Definitions Module

The house definitions module,, 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.

"" 2.2 =
import xmlrpc.client import datetime CENTRAL="terang" # system-wide definition of the house-controlling relay complement RelayNames=[ 'ChookUp', # 0 - the order of these is important 'ChookDown', # 1 'SouthVegBed', # 2 'GardenSteps', # 3 'Spare4', # 4 - unused from here, 'MiddleVegBed', # 5 - and unimplemented from here. 'TopVegBed', # 6 'CarportVegBed', # 7 'RainForest', # 8 'Woo2Plas', # 9 'FloodNDrain', # 10 'Heating' # 11 ] latitude = -37.8731753 # for 5 Fran Ct, Glen Waverley longitude = 145.1643351 NEFname="/home/ajh/Computers/House/events.txt" ThermostatSetting=19 colours=['#00f', # 10 '#04f','#08f','#0cf','#0ff', # 11-14 '#0fc','#0f8','#0f4','#0f0', # 15-18 '#4f0','#8f0','#cf0','#ff0', # 19-22 '#fc0','#f80','#f40','#f00'] # 23-26 NumberOfRelays = 5 # len(RelayNames) # changed in v2.0.0 RelayTable={} for i in range(NumberOfRelays): RelayTable[RelayNames[i]]=i <HouseDefinitions: server connections and interfaces 2.3> <HouseDefinitions: general routines 2.4>

2.2.1 HouseDefinitions: server connections and interfaces

<HouseDefinitions: server connections and interfaces 2.3> =
BeagleServerAdr=('',9999) NTempBlocks=7 # max number of distinct temperature blocks allowed HServer='http://%s:5000/heating' % (CENTRAL) # HeatingServer MServer='http://%s/~ajh/cgi-bin/' % (CENTRAL) # Main (web) server RServer='http://%s:8001' % (CENTRAL) # RelayServer EServer='<EventServerRPCaddress 8.2>' # EventServer SServer='http://%s:5000/solar' % (CENTRAL) # SolarServer TServer='http://%s:5000/tank' % (CENTRAL) # TankServer WServer='http://%s:5000/weather' % (CENTRAL) # WeatherServer RelayServerGood=True RelayServer=xmlrpc.client.ServerProxy(RServer) # check that the server is running by testing one of its interfaces try: RelayServer.getState() except: # bad response, let users know RelayServerGood=False EventServerGood=True EventServer=xmlrpc.client.ServerProxy(EServer) # check that the server is running by testing one of its interfaces try: dummy=EventServer.printEvents() except: # bad response, let users know EventServerGood=False
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): if NewLine: msg+='\n' if logging: logfile=open('/home/ajh/logs/terang/house.log','a') logfile.write("{}: {}".format(now.strftime("%H:%M:%S"),msg)) logfile.close() else: print(msg, end=' ') def setColourOld(temp): # return colours[temp-10] if temp>=ThermostatSetting: return 'red' else: return 'blue' def setTemperatureOld(arg): t=int(arg) if t>ThermostatSetting: t=ThermostatSetting if t<ThermostatSetting: t=10 return t def setColour(temp): return colours[temp-10] def setTemperature(arg): t=int(arg) return t
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.
The bottom layer software to hardware interface.
An interface to the outside world, providing primitive calls to control the attached relays.
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 - * * 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: * * Modified by Derek Molloy for the example on * 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 Restarting the BeagleBone Server

There are two issues:

  1. Setting up the device from scratch (e.g., system install), and
  2. After a reboot
The first is covered above in section GPIO Setup. What follows is what is needed after a reboot or power shutdown.

3.2.1 Restore the Pin Definitions

The Beaglebone is rather special in that it allows software reconfiguration of its I/O pins. For our purposes, we need to follow through on the configurations defined in section GPIO Setup, and setup the user interfaces to these hardware pins. Most of this is not necessary if the relevant programs run as root, but we change the permissions here so that users can also interact with the hardware.

The settings here are lost on a reboot, and so it is necessary to run this script after each reboot. Note that this script must be run as root.

"" 3.2 =
#!/bin/bash GPIO=/sys/class/gpio # output pins for i in 30 60 31 50 51 ; do echo $i >$GPIO/export done # input pins for i in 46 47 ; do echo $i >$GPIO/export done # make the outputs for i in 30 60 31 50 51 ; do echo "out" >$GPIO/gpio$i/direction echo 1 >$GPIO/gpio$i/value done # make the inputs for i in 46 47 ; do echo "in" >$GPIO/gpio$i/direction done # make user accessible chmod g+w /sys chmod g+w /sys/class chmod g+w /sys/class/gpio chgrp admin /sys chgrp admin /sys/class chgrp admin /sys/class/gpio for i in 30 60 31 50 51 46 47 ; do chgrp admin $GPIO/gpio$i ; done for i in 30 60 31 50 51 46 47 ; do chmod g+w $GPIO/gpio$i ; done for i in 30 60 31 50 51 46 47 ; do chmod g+w $GPIO/gpio$i/value ; done for i in 30 60 31 50 51 46 47 ; do chgrp admin $GPIO/gpio$i/value ; done

The key thing to note is the list of pin numbers 30 60 31 50 51, which define output pins. These numbers are used to set gpio definitions, and then to set directions (outputs) and initialize these outputs to 1 (which is the "off" state for the driven relays).

Next, we define the input pins 46 47.

Finally, we set various permissions to make sure that users (and not just root) can use these definitions.

3.2.2 Restart the BeagleBone Server proper

Once the pin definitions are restored, the BeagleBone server can be restarted. This is done with a simple

            $ ~/Computers/House/ &
See below for the code for the BeagleServer.

3.3 the program

"" 3.3 =
class relayDriver(): def __init__(self,virile): f0=open("/sys/class/gpio/gpio30/value",'w') f1=open("/sys/class/gpio/gpio60/value",'w') f2=open("/sys/class/gpio/gpio31/value",'w') f3=open("/sys/class/gpio/gpio50/value",'w') f4=open("/sys/class/gpio/gpio51/value",'w') self.valueFiles=[f0,f1,f2,f3,f4] self.values=[False for i in range(5)] self.reads=[0 for i in range(2)] self.virile=virile def switch(self,relay,value): if value: v="0" else: v="1" if self.virile: self.valueFiles[relay].write(v) self.valueFiles[relay].flush() self.values[relay]=value def read(self): res='' r0=open("/sys/class/gpio/gpio47/value",'r') r1=open("/sys/class/gpio/gpio46/value",'r') readfiles=[r0,r1] for i in range(2): self.reads[i]=readfiles[i].read().strip() res=res+"{}".format(self.reads[i]) r0.close();r1.close() return res def makeVirile(self): self.virile=True def makeSterile(self): self.virile=False def __str__(self): str="" for i in range(5): if self.values[i]: str+="o" else: str+="." return str

This code provides a class that drives the relays directly through the GPIO pins of the BeagleBone. The init method of the class creates a file for each GPIO pin in use. This file is available for writing, and by writing the appropriate value, 1 for on or 0 for off, to the file turns the nominated relay attached to the corresponding GPIO pin (labelled gpiopin number) on or off.

The second method, switch, turns a single bit/relay on or off. The input request is a pair of digits, the first of which defines the relay number to be used (0-origin), and the second digit of which defines the on (1) or off (0) desired state of this relay. Note that the file has to be flushed after writing, for the new data value to be transferred immediately. We preserve the new state in the class entity values.

The third and fourth methods control the activity of the driver. It can be switched into an ineffective mode, called sterile, by invoking the method makeSterile, and conversely, made active by invoking the method makeVirile. The flag entity virile controls this activity, and simply enables/disables the file writes in the switch method.

The fifth and last method renders the current saved state into a character string, which is used to return the new state to the calling environment. An 'o' in this string indicates that the corresponding relay is enabled, and inactive relays are shown as '.'.

3.4 The program

"" 3.4 =
#!/usr/bin/python # no python3 on this machine import datetime import SocketServer import re import BeagleDriver import sys BeagleServerAdr=('',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( value=int( 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': #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 print("{} BeagleServer starts".format(now)) laststate='' server.serve_forever()

This code runs a server to interface with the relay driver code ( 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 < 3.3>

3.5 The program

"" 3.5 =
#!/home/ajh/binln/python import socket import sys BeagleServerAdr=('',9999) data = " ".join(sys.argv[1:]) # Create a socket (SOCK_STREAM means a TCP socket) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: # Connect to server and send data sock.connect(BeagleServerAdr) sock.sendall(data + "\n") # Receive data from the server and shut down received = sock.recv(1024) finally: sock.close() print "Sent: {}".format(data) print "Received: {}".format(received)

A simple little program to test the operation of the Beagle relay software. It reads a pair of digits from the invoking CLI line, and passes them through to the relay driver via the server.

4. The Relay Server

Currently a Beagle Bone Black (see previous section) is used to interface to the house relays, and as general data logger and server for the various house functions. (An exception is the Chook Proving system, see section Chook Door Proving system.)

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

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

Possibilities for the new relays:

4.1 Relay Server Code

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

"" 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(('', 8001), requestHandler=RequestHandler, logRequests=False) server.register_introspection_functions() print("RelayServer registers RPC") # open the logfile logname="/home/ajh/logs/terang/RelayServer.log" logs=open(logname,'a') <RelayServer: connect to the BeagleServer 4.2> # define the relay state try: state=serverSend('') except: print("Cannot talk to the Beagle Server - have you started it?") sys.exit(1) currentState=[0 for i in range(NumberOfRelays)] for i in range(NumberOfRelays): if state[i]=='o': currentState[i]=1 currentTime =[0 for i in range(NumberOfRelays)] # time on in seconds nonZeroTimes=[] # those relays on for some time redundantChanges=[0 for i in range(NumberOfRelays)] # count idempotent ops <relayserver: strState 4.3> <relayserver: define the RPC-Server interface 4.4> # Define and Register the readDoor function <relayserver: readDoor 4.6> # Define and Register the getState function <relayserver: getState 4.5> # Define and Register the setState function <relayserver: setState 4.7> # Define and Register the setBit function <relayserver: setBit 4.8> # Define and Register the setBitOn function <relayserver: setBitOn 4.9> # Define and Register the setBitOff function <relayserver: setBitOff 4.10> # Define and Register the getTank function <relayserver: define getTank 4.11> # Define and Register the getTimer function <relayserver: getTimer 4.12> # Define and Register the start function <relayserver: start 4.14> # define the count down timers process <relayserver: countDown 4.15> # Define and Register the getSolar function <relayserver: getSolar 4.13> # Run the server's main loop"%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.
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.
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): 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:

Reads the state of the chicken door.
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).
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.
Set the relay identified by relay (numbered 0 and up) to the new state newState.
Set the relay identified by relay (numbered 0 and up) to 'On'.
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):"%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):"%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"%Y%m%d:%H%M%S") statefile='/home/ajh/logs/terang/tankState' p=open(statefile) l=p.readline() p.close() res=re.match('^(\d\d\d\d\d\d\d\d:\d\d\d\d\d\d) +(\d+) ',l) if res: level=int( 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):"%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):"%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)"%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 Starting the Relay Server

This short script encapsulates all that is necessary to (re)start the relay server. It records the process ID in the file relayProcess so that when it is restarted, any previous invocation is removed properly.

It is invoked by the make script as make start-relay.

"" 4.16 = **** File not generated!
LOGDIR='/home/ajh/logs/terang' HOUSE='/home/ajh/Computers/House' BIN=${HOME}/bin if [ -f ${LOGDIR}/relayProcess ] ; then for p in `cat ${LOGDIR}/relayProcess` ; do kill -9 $p done fi rm ${LOGDIR}/relayProcess # use logging version for now /usr/bin/python ${HOUSE}/ >${LOGDIR}/RelayServer.log 2>&1 & #/usr/bin/python ${HOUSE}/ `${BIN}/getDevice arduino` >/dev/null 2>&1 & ps aux | grep "" | grep -v grep | awk '{print $2}' >${LOGDIR}/relayProcess

4.3 The Relay Controller Code

This is a simple standalone program used by cron jobs to turn on relays at various times (mainly watering) for fixed periods of time. It calls the RelayServer via RPC calls to actually drive the relays, and really serves only as a CLI parameter handler. The relay name, and the time it is to turn on are supplied by two CLI parameters. If other than two parameters (besides the program name) are supplied, a default is used.

This program is used only for testing the Relay Server, and is not used by the HouseMade system per se.

"" 4.17 =
#!/home/ajh/binln/python3 import time import os import sys import xmlrpc.client from HouseDefinitions import * def main(device,timeRunning): # get relay bit for device try: bitNo=RelayTable[device] except KeyError: print("bad relay function key %s" % (device)) sys.exit(1) # start the relay for timeRunning seconds (state,ok)=RelayServer.start(bitNo,timeRunning) print("relays set to %s" % (state)) pass if __name__ == '__main__': if len(sys.argv)==3: # get relay name device=sys.argv[1] # get relay time timer=int(sys.argv[2]) else: # default if insufficient parameters device='FloodNDrain' timer=20 main(device,timer)

5. The Events Module

This module contains definitions of the Event and EventList classes, suitable for importing into other modules. At the moment, there is no standalone (main) function.

"" 5.1 =
<Event class: definition 6.1> <EventList class: definition 7.1>

6. The Event Class

A significant shift in the design (from HouseMade v2.2.0) is the introduction of the Event class, to encapsulate data pertaining to an event. An event can now occur at any time in the future, and is day of the week aware, as well as time of the year. This has implications for seasonal variations, such as sunrise/sunset times, and for summer/winter changes.

<Event class: definition 6.1> =
class Event(): def __init__(self,dictn=None,time=None,weekday=None,day=None,month=None,\ device=None,operation=None): self.time=time self.weekday=weekday self.month=month self.device=device self.operation=operation if dictn: for k in dictn.keys(): setattr(self,k,dictn[k]) pass def dictRep(self,ev): for key in ev.keys(): setattr(self,key,ev[key]) return def __str__(self): rtn='{' rtn+='month={}, '.format(self.month) rtn+='day={}, '.format( rtn+='time={}, '.format(self.time) rtn+='device={}, '.format(self.device) rtn+='operation={}'.format(self.operation) rtn+='}' return rtn <Event class: compare two events 6.2>
Chunk referenced in 5.1

The Event class provides data that defines an event in the system. There are a number of attributes, defined as follows:

The month of the year, and integer in the range 1-12, representing the months January-December. A value of none indicates the event occurs every month of the year.
The day of the month on which the event occurs, an integer in the range 1-31. A value of None indicates that the event occurs every day of the month.
A value in the range 0-6, where 0 represents Sunday, and 6 represents Saturday. This is for events that only occur on specific days of the week. A value of None indicates that the event occurs on every day of the week.
A value in the form HH:MM, representing time in a 24 hour format, and defining the time of day at which an event occurs.
The name of the device for which the event occurs. This is a text field, and currently has the values ChookDoor, SouthVegBed, and GardenLights.
The parameter for the event. This is also a text field, and currently has the values on, off, up, down, or a string of digits, representing an integer value.

Note that an event can be created with zero or more initial values for these attributes through the use of optional parameters. The optional parameter dictn can be used with a dictionary of values (with keys as for the fields defied above) to initialize the event. These values override any defined with the other values.

<Event class: compare two events 6.2> =
def compare(self,a): if a.time > self.time: return -1 elif a.time < self.time: return 1 else: return 0 def __gt__(self,other): return def __lt__(self,other): return def __eq__(self,other): return self.time==other.time
Chunk referenced in 6.1

Compare this event against another. For comparison purposes, only the times are relevant. If this event occurs first, return -1. If it occurs at the same time as event a, return 0. Otherwise, return 1.

7. The EventList class

<EventList class: definition 7.1> =
EVENTFILE="/home/ajh/etc/events.txt" import re class EventList(): def __init__(self): self.list=[] def __str__(self): rtn='' for e in self.list: rtn+="{}\n".format(e) return rtn <EventList class: add event 7.2> <EventList class: delete event 7.3> <EventList class: nextEvent 7.5> <EventList class: sort events 7.4> <EventList class: load events 7.6> <EventList class: save events 7.7>
Chunk referenced in 5.1

The EventList maintains a list of events, sorted chronologically. Past events are possible, and are ignored for scheduling purposes.

<EventList class: add event 7.2> =
def add(self,e,dupl=False): # e is an Event, dupl is boolean, True=>duplicates allowed #print("adding {}, is type {}, duplicate={}".format(e,type(e),dupl)) if not self.list: self.list=[e]; return for l in self.list: #print ("check event {}, type is {}".format(l,type(l))) same=True # until proven false for attr in ['month','day','time','device','operation']: if getattr(l,attr)!=getattr(e,attr): same=False ; break #print("Adding Event, same={}, duplicate={}".format(same,dupl)) if same and not dupl: print("Event {} not added - duplicate event".format(e)) return if < 0: i=self.list.index(l) self.list.insert(i,e) return self.list.append(e)
Chunk referenced in 7.1

When adding an event, it is placed into the list according to its time attribute, so that the preceding event has a time value that is the same (see <Event class: compare two events >, and the following event has a time that is the same or later.

The dupl parameter, when True, allows duplicate events to be added. Normally, duplicate events cannot be added when this parameter is False

<EventList class: delete event 7.3> =
def delete(self,a): # a is an Event self.list.remove(a) return
Chunk referenced in 7.1

Remove a given event from the list.

<EventList class: sort events 7.4> =
def sort(self): old=self.list self.list=[] for e in old: self.add(e) return
Chunk referenced in 7.1

(Re)sort the event list. This is done with an insertion sort, to ensure that the list is recreated in the same order as when events are added one by one.

<EventList class: nextEvent 7.5> =
def nextEvent(self,now): # return index of first event with time =/> now #print(self.list) i=0 for e in self.list: #print(e) if now<=e.time: return i i+=1 return None
Chunk referenced in 7.1

Return the first event in the list with time equal or later to the given time stamp now. Return None if there are no such events. (This is a change in the definition - previously the last event would be returned in this case, which violates the postcondition.)

<EventList class: load events 7.6> =
def load(self): f=open(EVENTFILE,'r') for l in f.readlines(): res=re.match('{month=(.*), day=(.*), time=(.*), device=(.*), operation=(.*)}',l) if res: ev=Event(,, \,,\ self.add(ev) else: print("Cannot parse {}".format(l)) f.close() return
Chunk referenced in 7.1

Load events from a file. Existing events are retained, and new events are inserted to preserve the chronological ordering.

<EventList class: save events 7.7> =
def save(self): f=open(EVENTFILE,'w') content=self.__str__() f.write(content) f.close() return
Chunk referenced in 7.1

Save all current events to a file, which may be later reloaded with load. Be aware that on reloading, any existing events with the same parameters will become duplicate events.

8. The Event Server

<EventServerRPCport 8.1> = 8002
Chunk referenced in 8.2 8.3
<EventServerRPCaddress 8.2> = http://terang:<EventServerRPCport 8.1>
Chunk referenced in 2.3 9.1 10.1

Define the address of the EventServer port.

"" 8.3 =
#!/home/ajh/binln/python3 import datetime import sys import time from Events import Event,EventList class ShutDown(Exception): pass from multiprocessing import Process from xmlrpc.server import SimpleXMLRPCServer from xmlrpc.server import SimpleXMLRPCRequestHandler # data structures el=EventList() # Restrict to a particular path. class RequestHandler(SimpleXMLRPCRequestHandler): rpc_paths = ('/RPC2',) # Create server port=('', <EventServerRPCport 8.1>) server = SimpleXMLRPCServer(port, requestHandler=RequestHandler, allow_none=True) server.register_introspection_functions() #print("EventServer registers RPC") # identify the next event. None means not yet identified. # Caveat! nextEventPointer can take the value 0, meaning the first # event. Be careful to distinguish 0 from None, they both test False! nextEventPointer=None <Event Server: log message handling 8.4> <Event Server: calling points 8.5> <Event Server: serverprocess routine 8.6> <Event Server: main routine 8.7> if __name__ == '__main__': main()

The EventServer is a development of the EventManager as a means of providing a generic event service.

8.1 Event Server Log Message Handling

<Event Server: log message handling 8.4> =
# open the logfile logname="/home/ajh/logs/terang/EventServer.log" logs=open(logname,'a') logs.write("\n") # space from any previous log messages def logmsg(msg): nowstr=now.strftime("%Y%m%d:%H%M%S") logs.write("{}: {}\n".format(nowstr,msg)) logs.flush()
Chunk referenced in 8.3

8.2 Event Server calling points

<Event Server: calling points 8.5> =
def add(evd,dupl,handle=None): # evd comes in as a dictionary, must convert it ... ev=Event() # first make empty event for key in evd.keys(): setattr(ev,key,evd[key]) el.add(ev,dupl=dupl) # note that add always adds in order logmsg("server adds event {}".format(ev)) return server.register_function(add, 'add') def remove(evn): if type(evn) is int: # delete event number evn if evn<len(el.list): del el.list[evn] return elif type(evn) is Event: # find event that matches evn in device and operation pass # needs work! server.register_function(remove, 'remove') def setNext(curtime): '''Use the current time to identify the next event to be scheduled, and then set nextEventPointer to point to this event. nextEventPointer is advanced by getNext.''' global nextEventPointer if not curtime: curtime=now.strftime("%H:%M") nextEventPointer=el.nextEvent(curtime) server.register_function(setNext, 'setNext') def advanceNext(): '''move to the next event in the list''' global nextEventPointer if nextEventPointer is None: nextEventPointer=0 elif nextEventPointer < len(el.list): nextEventPointer+=1 return server.register_function(advanceNext, 'advanceNext') def showNext(): '''return the next event in the list. The nextEventPointer is not advanced.''' global nextEventPointer if nextEventPointer is None: return None if nextEventPointer >= len(el.list): return None ev=el.list[nextEventPointer] logmsg("show next event returns event number {}, which is {}".format(nextEventPointer,ev)) return ev server.register_function(showNext, 'showNext') def moreEvents(): global nextEventPointer if nextEventPointer is not None and nextEventPointer < len(el.list): return True else: return False server.register_function(moreEvents, 'moreEvents') def printEvents(): for ev in el.list: print(ev) return server.register_function(printEvents, 'printEvents') def getEvent(i): if i>=len(el.list): return None else: return el.list[i] server.register_function(getEvent, 'getEvent') def matchEvents(ev): # return a list of events matching ev in device and operation l=[] for e in el.list: if e.device==ev.device and e.operation==ev.operation: l.append(e) return l server.register_function(matchEvents, 'matchEvents') def sortEvents(): el.sort() server.register_function(sortEvents, 'sortEvents') def loadEvents(): el.load() server.register_function(loadEvents, 'loadEvents') def saveEvents(): server.register_function(saveEvents, 'saveEvents') def registerCallback(device,routine): dispatcher[device]=routine server.register_function(registerCallback, 'registerCallback')
Chunk referenced in 8.3

These are the entry points for calls on the EventServer. The logmsg are commented out until I can work out how to output messages from a server subprocess (it involves using a Queue or a Pipe, see The Python3 Library).

8.3 Event Server: serverprocess routine

<Event Server: serverprocess routine 8.6> =
def serverprocess(logmsg): try: server.serve_forever() except KeyboardInterrupt: pass
Chunk referenced in 8.3

The serverprocess routine is the heart of the event server. Its role is to package up the separate process of running the server, while allowing the event manager to manage its events. Not sure about the KeyboardInterrupt, though.

8.4 Event Server: main routine

<Event Server: main routine 8.7> =
def main(testing=True,forReal=True): logmsg("EventServer starts serving on port {}".format(port)) serverp = Process(target=serverprocess, args=(logmsg,)) serverp.start() try: serverp.join() except KeyboardInterrupt: logmsg("Keyboard Interrupt received! ") nowstr=now.strftime("%Y%m%d:%H%M%S") logmsg("EventServer stops serving (main)") logs.close()
Chunk referenced in 8.3

9. The Event Editor

This is a new component to the HouseMade system. It provides a web interface to the EventServer, allowing the user to view and edit the current list of events.

"" 9.1 =
#!/home/ajh/binln/python3 import cgi import datetime import os,sys from Events import Event,EventList import xmlrpc.client nowstr=now.strftime("%Y%m%d:%H%M") import cgitb cgitb.enable() EServer='<EventServerRPCaddress 8.2>' es=xmlrpc.client.ServerProxy(EServer,allow_none=True) print("Content-type: text/html\n\n") form=cgi.FieldStorage() #print(form) print('<h1 style="text-align:center; background-color:lightgreen">HouseMade Events Editor</h1\n') <EventEditor: print instructions 9.4> <EventEditor: define get current events routine 9.5> <EventEditor: define make home page routine 9.6> <EventEditor: define make edit page routine 9.7>
Chunk defined in 9.1,9.2,9.3

Define global stuff, then the various routines to perform the EventEditor operations.

"" 9.2 =
try: el=getAllEvents() except ConnectionRefusedError: print('<p style="background-color:red; font-style:italic;color:white">',end='') print('Connection to EventServer refused - is it running?</p>') sys.exit(0) #print(el) if 'action' in form: request=form['action'].value else: request='home' entry=None if 'entry' in form: entry=int(form['entry'].value) month=day=time=device=operation='' if 'month' in form: month=form['month'].value if 'day' in form: day=form['day'].value if 'time' in form: time=form['time'].value if not ':' in time: time="{:s}:{:s}".format(time[0:2],time[2:4]) if 'device' in form: device=form['device'].value if 'operation' in form: operation=form['operation'].value #print("<p>Request is for {}, entry={}</p>".format(request,entry))
Chunk defined in 9.1,9.2,9.3

Now collect the events from the server, and the edit parameters from the URL form.

"" 9.3 =
if request=='home': page=makeHomePage() elif request=='add': # Need to know where to add this entry (given by value of entry) # added after entry number 'entry' with time set to that of entry number entry # adding to empty list is a special case if len(el.list)==0: # make first entry event a=Event() a.time='00:00' else: # get event at this entry number e=es.getEvent(entry) a=Event(dictn=e) print("Adding event {}".format(a)) es.add(a,dupl=True) #print(len(el.list)) page=makeHomePage() elif request=='edit': page=makeEditPage(entry,month=month,day=day,time=time,device=device,operation=operation) elif request=='delete': print("<p>deleting entry number {}</p>".format(entry)) es.remove(entry) page=makeHomePage() else: page="<p>Action <i>{}</i> not implemented</p>".format(request) print("{}".format(page))
Chunk defined in 9.1,9.2,9.3

Examine the request, and perform the appropriate task. add adds a new event to the table, using the current event as the default value. edit updates any edited information, and delete deletes the given element.

9.1 The EventEditor Instructions

<EventEditor: print instructions 9.4> =
print(''' <p> Events are shown, one per line in the table. '*' for <b>Month</b> and <b>Day</b> indicate every month/day is involved. (These can be names from the lists Jan-Dec or Sun-Sat, but this is not implemented as yet.) </p> <p> <b>Times</b> are to be entered in 24 hour format, either as 'HH:MM' or as 'HHMM' (but are stored and displayed in the first format). The <b>Device</b> will be a pull-down from a menu, but is currently implemented only as a text field. Similarly for the <b>Operation</b> field. </p> <p> The <b>Change</b> column buttons save/enter any changed data on the given line. <b>Add</b> adds a new event, initialized to the data on the given line. <b>Delete</b> will delete the event. Note that if you need to add an event before the first entry, simply click on <b>Add</b> for the first entry, and edit the time field to be sooner than the given time. </p> <p> The <b>NOW</b> line indicates the current time in the sequence of events, and the next line will be the next event to be scheduled. </p> ''')
Chunk referenced in 9.1

9.2 EventEditor: get current events routine

<EventEditor: define get current events routine 9.5> =
# get all the current events def getAllEvents(): i=0 es.sortEvents() x=es.getEvent(0) el=EventList() while x: #print(x) ev=Event(dictn=x) el.add(ev) i+=1 x=es.getEvent(i) #el.sort() return el
Chunk referenced in 9.1

9.3 EventEditor: Make Home Page

<EventEditor: define make home page routine 9.6> =
def makeHomePage(): es.sortEvents() el=getAllEvents() numEvents=len(el.list) nowTime=now.strftime("%H:%M") #print("<p>at start of makeHomePage, el={}".format(el)) page='<p>Current events are:</p>\n' page+=' <table style="margin-left:40pt;" border="4pt solid green">\n' page+=' <tr><th>Month</th><th>Day</th><th>Time</th><th>Device</th><th>Operation</th>' addstr='Add<br/>After' if numEvents==0: addstr='<form action="http://terang/~ajh/cgi-bin/" method="post">' addstr+='<button type="submit" value="0">Add</button>' addstr+='</form>' page+=' <th>Save<br/>Changes</th><th>{}</th><th>Delete this entry</th>'.format(addstr) page+=' </tr>\n' i=0 ; doneNow=False for ev in el.list: line=' <tr>\n' if ev.time and not doneNow and nowTime < ev.time: line+='<td colspan="2" align="center" bgcolor="pink">NOW</td>' line+='<td>{}</td>'.format(nowTime) line+='<td colspan="5" align="center" bgcolor="pink">NOW</td></tr><tr>\n' doneNow=True month=ev.month if not month: month='*' line+=' <form action="http://terang/~ajh/cgi-bin/{}" method="post">\n'.format(i) line+=' <td><input type="text" name="month" value="{}"></input></td>\n'.format(month) if not day: day='*' line+=' <td><input type="text" name="day" value="{}"></input></td>\n'.format(day) time=ev.time line+=' <td halign="center"><input align="center" type="text" name="time" value="{}"></input></td>\n'.format(time) device=ev.device #line+='<td><form>' #line+=' <select name="device">' #for dev in ['ChookDoor','SouthVegBed','GardenLights']: # if dev==device: selected=' selected' # else: selected='' # line+=' <option value="{}{}">{}</option>'.format(dev,selected,dev) #line+=' </form></select>' #line+='</td>\n' line+=' <td><input type="text" name="device" value="{}"></input></td>\n'.format(device) operation=ev.operation line+=' <td><input type="text" name="operation" value="{}"></input></td>\n'.format(operation) # Enter column line+=' <td>\n' line+=' <button type="submit" value="{}">Enter</button></td>\n'.format(i) line+=' </td>\n' line+=' </form>\n' # Add column line+=' <form action="http://terang/~ajh/cgi-bin/{}" method="post">\n'.format(i) line+=' <td align="center" valign="middle">\n' line+=' <button type="submit" value="{}">Add</button>\n'.format(i) line+=' </td>\n' line+=' </form>\n' # Delete column line+=' <form action="http://terang/~ajh/cgi-bin/{}" method="post">\n'.format(i) line+=' <td style="align:center; valign:bottom">\n' line+=' <button type="submit" value="{}">Delete</button>\n'.format(i) line+=' </td>\n' line+=' </form>\n' line+=' </tr>\n' page+="{}".format(line) i+=1 if not doneNow: page+='<td colspan="8" align="center" bgcolor="pink">NOW</td></tr><tr>\n' page+=' </table>\n' page+='</p>\n' return page
Chunk referenced in 9.1

9.4 EventEditor: Make Edit Page

<EventEditor: define make edit page routine 9.7> =
def makeEditPage(entry,time=None,weekday=None,day=None,month=None,\ device=None,operation=None): el=getAllEvents() if entry>= len(el.list): ev=Event() else: ev=el.list[entry] es.remove(entry) ev.month=month ev.time=time ev.device=device ev.operation=operation es.add(ev) return makeHomePage()
Chunk referenced in 9.1

10. The Event Scheduler

This program takes the place of the old EventManager, and uses the EventServer to maintain the collection of events to be scheduled. Its basic algorithm is much the same: look at the next event from the current time, wait until its time to be scheduled has arrived, and then schedule it.

Some thought does need to be given to events that may be added to the event list between now and the scheduling time (should they be ignored, should they take precedence of the current next event, etc.). A possible solution: if the next event's time is now, then schedule it. Otherwise, sleep for a minute, and then recheck the next event. It is possible that the wait time should be less than a minute, to allow for processing time, which could see the wait time to be longer than a minute, thereby missing the scheduling time of an event.

Also, if two or more events are scheduled for the same time, only one will be scheduled. It is indeterminate which of the simultaneous events will be scheduled. This needs further careful thought.

Some thoughts. Create a 'mark now' which identifies the initial next event time. Set a pointer to mark the next event. Provide an interface to inspect the next event (but keep the same marker). Provide a separate 'move marker' to advance to the next event. This should get around the duplicate schedule time problem. But do need to check times of events, particularly that we don't fall behind in processing duplicate events.

"" 10.1 =
#!/home/ajh/binln/python3 import datetime from Events import Event,EventList import sys import time import xmlrpc.client import GardenSteps import GardenWater import ChookDoor print("EventScheduler starts") # connect to the server EServer='<EventServerRPCaddress 8.2>' es=xmlrpc.client.ServerProxy(EServer,allow_none=True) # dispatcher is a dictionary of call-back routines indexed by device, and # called when scheduled with the given parameter for the event. dispatcher={} gs=GardenSteps.GardenSteps() dispatcher['GardenSteps']=gs.handleEvent gw=GardenWater.GardenWater() dispatcher['SouthVegBed']=gw.handleEvent cd=ChookDoor.ChookDoor() dispatcher['ChookDoor']=cd.handleEvent es.loadEvents() try: lastTime=None nowTime=now.strftime("%H:%M") es.setNext(nowTime) while es.moreEvents(): next=es.showNext() ev=Event(dictn=next) print("at {}, next event is {}".format(now.strftime("%H%M%S"),ev)) while ev.time==nowTime: print("Schedule event {}".format(ev)) dev=ev.device; op=ev.operation if dev in dispatcher: dispatcher[dev](dev + ' ' + op) print("Event {}({}) dispatched".format(dev,op)) else: print("No handler for event") es.advanceNext() next=es.showNext() ev=Event(dictn=next) nowTime=now.strftime("%H:%M") secs2zero=60-now.second time.sleep(secs2zero) es.setNext(nowTime) pass except KeyboardInterrupt: print("EventScheduler terminated by KeyboardInterrupt") es.saveEvents() sys.exit(0) print("EventScheduler runs out of events") es.saveEvents()

11. The Chook Door

This section is being rebuilt, and obsoletes the old section of the same name. The philosophy used is quite different, and is based on an event handling model. This module is the first to be rebuilt according to this model.

This code has been migrated from HouseMade and slightly modified to now work with the EventServer model. The original has been changed to ChookDoor to avoid confusion. Currently (version 1.0.4) the interface does not handle sunrise and sunset times.

The chook house (described separately in the Chickens page) has a door that is automatically controlled, and opens and shuts in accordance with sunrise and sunset times throughout the year.

The EventScheduler module has the responsibility for driving the chook door events. Each module requiring action at a particular time needs to register with the event scheduler with a request to be alerted when a particular time is reached. This may be a time of day, or every minute, or every hour, or even a day of the week.

This ChookDoormodule just acts as a passive interface, providing the necessary code to actually perform the chook door operation. The are two base level operations: openDoor and closeDoor, with obvious meanings. They communicate by means of a socket connection on port 9999 to the relay server running on kerang, accessed through the generic interface serverSend.

A third operation chookDoor(p) uses a parameter p to indicate whether an open or close operation is required. Either of 'open'/1 can be used to invoke the opening, or 'close'/'shut'/0 to close the door.

A fourth operation, doorState, can be used to interrogate the current door status.

The routine handleEvent, is the generic interface to the EventScheduler, and bundles all the driver routines.

Important Note: This interface may change later, when the relay drivers are moved to the new house computer, ouyen. Then the code will be moved to a generic Relay Driver.

"" 11.1 =
import socket import sys import time #import currentState import datetime #import eventManager import getopt import re import sys from HouseDefinitions import * from suntime import Sun, SunTimeException class BadChook(BaseException): pass compute=0 version='1.0.0' latitude = -37.8732083333333 # for Glen Waverley longitude = 145.164671666667 chookFileName='/home/ajh/Computers/House/suntimes.txt' timezone=datetime.timezone(datetime.timedelta(hours=10)) <ChookDoor: misc routines 11.2> <ChookDoor: class ChookDoor 11.3> <ChookDoor: main 11.15> if __name__ == '__main__': (vals,path)=getopt.getopt(sys.argv[1:],'cVn=', ['compute','now=','version']) for (opt,val) in vals: if opt=='-c' or opt=='--compute': compute=1 if opt=='-n' or opt=='--now': now=datetime.datetime.strptime(val,"%Y%m%d:%H%M") if opt=='-V' or opt=='--version': print(version) sys.exit(0) main()

11.1 ChookDoor: misc routines

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

11.2 ChookDoor: class ChookDoor

<ChookDoor: class ChookDoor 11.3> =
class ChookDoor(): <class ChookDoor: init 11.4> <class ChookDoor: load 11.5> <class ChookDoor: compute 11.6> <class ChookDoor: save 11.7> <class ChookDoor: openDoor 11.8> <class ChookDoor: closeDoor 11.9> <class ChookDoor: chookDoor 11.10> <class ChookDoor: doorState 11.11> <class ChookDoor: handleEvent 11.12> <class ChookDoor: run 11.13> <class ChookDoor: stop 11.14>
Chunk referenced in 11.1

11.2.1 class ChookDoor: init

<class ChookDoor: init 11.4> =
def __init__(self): self.debug=False self.lastDoorState='unknown' 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 11.3

11.2.2 class ChookDoor: load

<class ChookDoor: load 11.5> =
def load(self): opendelay=0; shutdelay=0 try: suntimefile=open(chookFileName,'r') innow=suntimefile.readline() self.lastrun=parse('now += (.*)$',innow) 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 11.3

11.2.3 class ChookDoor: compute

<class ChookDoor: compute 11.6> =
def compute(self): 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) 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 self.whichsrday='today' else: self.whichsrday='tomorrow' if self.whichssday='today' else: self.whichssday='tomorrow' lastState=self.current
Chunk referenced in 11.3

11.2.4 class ChookDoor: save

<class ChookDoor: save 11.7> =
def save(self): suntimefile=open(chookFileName,'w') suntimefile.write("now = %s\n" % ("%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 11.3

11.2.5 class ChookDoor: Open Chook Door

<class ChookDoor: openDoor 11.8> =
def openDoor(self): if self.debug: print("(debug) Opening Chook Door") self.lastDoorState='close' return RelayServer.setBitOn(RelayTable['ChookUp']) time.sleep(2) RelayServer.setBitOff(RelayTable['ChookUp']) self.lastDoorState='close' print("ChookDoor has been opened")
Chunk referenced in 11.3

11.2.6 class ChookDoor: Close Chook Door

<class ChookDoor: closeDoor 11.9> =
def closeDoor(self): if self.debug: print("(debug) Closing Chook Door") self.lastDoorState='open' return RelayServer.setBitOn(RelayTable['ChookDown']) time.sleep(2) RelayServer.setBitOff(RelayTable['ChookDown']) self.lastDoorState='open' print("ChookDoor has been closed")
Chunk referenced in 11.3

11.2.7 class ChookDoor: chookDoor

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

11.2.8 class ChookDoor: doorState

<class ChookDoor: doorState 11.11> =
def doorState(self): r=RelayServer.readDoor() if r=='11': if self.lastDoorState=='open': return 'movingdown' elif self.lastDoorState=='close': return 'movingup' return 'door moving' elif r=='01': self.lastDoorState='open' return 'open' elif r=='10': self.lastDoorState='close' return 'closed' else: raise(BadChook)
Chunk referenced in 11.3

11.2.9 class ChookDoor: handleEvent

<class ChookDoor: handleEvent 11.12> =
def handleEvent(self,parms): ps=parms.split(' ') device=ps[0] onoff=ps[1] try: self.chookDoor(onoff) except: raise(BadChook)
Chunk referenced in 11.3

11.2.10 class ChookDoor: run

<class ChookDoor: run 11.13> =
def run(self,em,debug): self.debug=debug self.load() self.compute() #print(self.dooropen) # strip open and close times to hours:minutes res=re.match('(\d{2}):(\d{2})',self.dooropen)':' res=re.match('(\d{2}):(\d{2})',self.doorshut)':' 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 11.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.

11.2.11 class ChookDoor: stop

<class ChookDoor: stop 11.14> =
def stop(self): # just print a message for now print("ChookDoor handler now terminating")
Chunk referenced in 11.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.

11.3 ChookDoor: main

<ChookDoor: main 11.15> =
def main(): print("Running ChookDoor.main") chooks=ChookDoor() chooks.load() if compute: chooks.compute() 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 11.1

12. The Garden Steps Lighting

This module follows the same model as the (revised) ChookDoor module, namely that it defines GardenSteps.handleEvent method that interfaces to the EventScheduler.

"" 12.1 =
#!/home/ajh/binln/python3 from HouseDefinitions import * from suntime import Sun, SunTimeException import datetime class GardenSteps(): def __init__(self): self.debug=0 self.ontime='' self.offtime='' self.ondelay=5 self.offdelay=0 # invoke the suntime routine to find sunrise and sunset sun = Sun(latitude, longitude) # Get today's sunrise and sunset in localtime #sunrise = sun.get_local_sunrise_time() # don't care about sunrise sunset = sun.get_local_sunset_time() ontm=sunset+datetime.timedelta(0,0,0,0,int(self.ondelay)) offtm=datetime.time(22,0) # use just fixed time for now - was ... #sunset+datetime.timedelta(0,0,0,0,int(self.offdelay)) # compute desired on and off times self.onTime=ontm.strftime("%H:%M") self.offTime=offtm.strftime("%H:%M") def switchOn(self): if not self.debug: 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: print("Garden Steps lights are switched off at {}".format(now)) RelayServer.setBitOff(RelayTable['GardenSteps']) else: print("(debug) Garden Steps lights are switched off") return def switch(self,onoff): if onoff: self.switchOn() else: self.switchOff() return def handleEvent(self,parms): ps=parms.split(' ') device=ps[0] onoff=ps[1] if onoff in ['1','on']: self.switchOn() elif onoff in ['0','off']: self.switchOff() def run(self,em,debug): self.debug=debug # register these events onEv=('*',self.onTime,'gardensteps','on',None) em.registerEvent(onEv,self.handleEvent) offEv=('*',self.offTime,'gardensteps','off',None) em.registerEvent(offEv,self.handleEvent) # save the on and off times for the web page stepsTimesf=open('/home/ajh/Computers/House/stepsTimes.txt','w') stepsTimesf.write("{} (On Time)\n".format(self.onTime)) stepsTimesf.write("{} (Off Time)\n".format(self.offTime)) stepsTimesf.close() return def stop(step): print("GardenSteps handler now terminating") # not much required as of yet return

13. The Garden Watering System

This module follows the same model as the (revised) ChookDoor module, namely that it defines GardenWater.handleEvent method that interfaces to the EventScheduler.

A major difference with the GardenSteps module, however, is that there is more than one sprinkler. Hence the sprinkler involved has to be passed as a parameter to the event handling routine, and the on/off routines. Currently there is only one sprinkler, though: the SouthVegBed. The dispatcher in the EventScheduler defines a handler for each distinct water section, even though in each case the handler is the same (GardenWater.handleEvent). The distinction between the different watering sections is through the device name, passed as a parameter through the handleEvent methos.

"" 13.1 =
#!/home/ajh/binln/python3 from HouseDefinitions import * from suntime import Sun, SunTimeException import datetime import re class GardenWater(): def __init__(self): self.debug=0 self.onTime='09:00' self.offTime='09:15' def switchOn(self,sprinkler): if not self.debug: 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: print("Garden Water Sprinkler {} turned off at {}".format(sprinkler,now)) RelayServer.setBitOff(RelayTable[sprinkler]) else: print("(debug) Garden Water Sprinkler {} turned off".format(sprinkler)) return def switch(self,onoff,sprinkler): if onoff: self.switchOn(sprinkler) else: self.switchOff(sprinkler) return def handleEvent(self,parms): print("Garden Watering parameters are {}".format(parms)) ps=parms.split(' ') device=ps[0] onoff=ps[1] if onoff in ['1','on']: self.switchOn(device) elif onoff in ['0','off']: self.switchOff(device) def run(self,em,debug): self.debug=debug # register these events onEv=('*',self.onTime,'gardenwater','SouthVegBed on',None) em.registerEvent(onEv,self.handleEvent) offEv=('*',self.offTime,'gardenwater','SouthVegBed off',None) em.registerEvent(offEv,self.handleEvent) # save the on and off times for the web page waterTimesf=open('/home/ajh/Computers/House/waterTimes.txt','w') waterTimesf.write("{} (On Time for {})\n".format(self.onTime,'SouthVegBed')) waterTimesf.write("{} (Off Time for {})\n".format(self.offTime,'SouthVegBed')) waterTimesf.close() return def stop(step): print("GardenWater handler now terminating") # not much required as of yet return

14. The Web Interface

The web interface is cgi application running on the house computer ouyen, and providing a conventional web page via a port 80 call (the http port number), and interfaces to the house and timer modules through house and heating respectively.

The figure '10' in setTemperature is just the lowest temperature that is displayed by the timer web interface.

To preserve security of the system, and prevent unauthorized access, this web server will only operate house functions if it is invoked from a machine on the 10.0.0 network (the private house network). In the longer term, username/password authority may be added.

14.1 The cgi application

"" 14.1 =
#!/home/ajh/binln/python3 import cgi import datetime import HouseMade import os 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),server,form) print(page)

This cgi script simply collects a few parameters, and then passes control to the house interface in the HouseMade module. The latter module has the responsibility of generating the web page, which is returned as a string to be printed (rendered) by this cgi script.

14.2 The HouseMade module

"" 14.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/' % (CENTRAL) # Main (web) server <HouseMade: define the Generate Weather Data routine 14.9> <HouseMade: define the Generate Solar Data routine 14.10> <HouseMade: define the Generate Tank Data section 14.11> <HouseMade: define the house interface 14.3> if __name__=='__main__': house() ## ## The End ##

14.2.1 The house interface

<HouseMade: define the house interface 14.3> =
def house(remadr,server,args): import os,sys DEBUG=False <HouseMade: collect date and time data 14.12> <HouseMade: check client connection 14.13> # 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 14.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 14.5> # relayStateStr is a string containing the relay state information ################################################## EVENTS INFORMATION ######## # Get the list of events from the Event Server and display them <HouseMade: get events information 14.6> # eventsInfo is a string containing the events 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 14.7> # 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" # this idempotent assignment is because MServer is not a local # variable, and we have to get its content into a local variable for # use in the next u link! serverlink=MServer housepage=<HouseMade: generate the web page content 14.14> return housepage
Chunk referenced in 14.2

14.2.2 Get Local Information

<HouseMade: get local information 14.4> =
# Firstly, chook relevant times import ChookDoor chooks=ChookDoor.ChookDoor() chooks.compute() #chooks.load() localrise=chooks.sunrise localset=chooks.sunset gateopentime=chooks.dooropen gateshuttime=chooks.doorshut import GardenSteps steps=GardenSteps.GardenSteps() stepsOn=steps.onTime stepsOff=steps.offTime dayOpen=chooks.whichsrday.capitalize() dayShut=chooks.whichssday import GardenWater water=GardenWater.GardenWater() waterOn=water.onTime waterOff=water.offTime localinfo=''
Chunk referenced in 14.3

14.2.3 Get Relay Information

<HouseMade: get relay information 14.5> =
RelayState=RelayServer.getState() # here process any switching requests # respond to any argument requests - can only do if RPC server present active=False if args and HouseDefinitions.RelayServerGood: currState=RelayServer.getState() for relay in RelayNames: if relay in args: active=True bitNo=RelayTable[relay] newState=args[relay].value if newState in ['off','on']: doWhat={'off':RelayServer.setBitOff,'on':RelayServer.setBitOn} change=doWhat[newState](bitNo) #print("change bit %d(%s) to %s" % (bitNo,relay,newState)) else: # start timer with time == newstate timerCount=int(newState) RelayServer.start(bitNo,timerCount) #print("timer started for bit %d(%s) for %d" % \ # (bitNo,relay,timerCount)) # just to confirm any changes RelayState=RelayServer.getState() #print(RelayState);time.sleep(5) # deal with chook door state #curdoorstate=RelayServer.readDoor() #chookdoorlabel='open' # until proved otherwise #if curdoorstate[0]=='0': # door is now up, turn off ChookUp # RelayServer.setBitOff(RelayTable['ChookUp']) # chookdoorlabel='open' # established #elif curdoorstate[1]=='0': # door is now down, turn off ChookDown # RelayServer.setBitOff(RelayTable['ChookDown']) # chookdoorlabel='closed' #else: # if RelayState[RelayTable['ChookDown']]: # chookdoorlabel='movingdown' # active=True # if RelayState[RelayTable['ChookUp']]: # chookdoorlabel='movingup' # active=True chookdoorlabel=chooks.doorState() # just to confirm any changes RelayState=RelayServer.getState() currentcircuits=[] for i in range(NumberOfRelays): if RelayState[i]: currentcircuits.append(RelayNames[i]) if len(currentcircuits) > 1: currentcircuits = "the " + ', '.join(currentcircuits[:-1]) + " and " + currentcircuits[-1] elif len(currentcircuits) > 0: currentcircuits = "the " + currentcircuits[0] else: currentcircuits = "no" relayStateStr=''' <p> Currently %(currentcircuits)s circuits are on. </p> ''' % vars() #relayStateStr+=''' # Visit the <a href="%(HServer)s">Heating Timer</a> page; # <a href="%(WServer)s">Weather</a>; # <a href="%(SServer)s">Solar Power</a>; # <a href="%(TServer)s">Tank Storage</a>. # ''' % vars(HouseDefinitions)
Chunk referenced in 14.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.

14.2.4 Get Events Information

<HouseMade: get events information 14.6> =
eventsInfo='' i=0; evlist=[] while True: ev=EventServer.getEvent(i) if ev: evlist.append(ev);i+=1 else: break if i>0: nowTime=now.strftime("%H:%M") eventsInfo+='<h3>Previously scheduled events for today</h3>\n<table>\n' nextHead='</table><h3>{} Now</h3><table>'.format(nowTime) for e in evlist: if e['time']>nowTime: eventsInfo+=nextHead nextHead='' eventsInfo+='<tr>\n' eventsInfo+='<td width="100px">{}</td>'.format(e['time']) eventsInfo+='<td width="200px">{}</td>'.format(e['device']) eventsInfo+='<td width="100px">{}</td>'.format(e['operation']) eventsInfo+='</tr>\n' eventsInfo+='<table>\n' else: eventsInfo += '<p>No events found</p>'
Chunk referenced in 14.3

14.2.5 Legacy Code

<HouseMade: legacy code for 14.7> =
(aimtemp,onoff) = (ThermostatSetting,'off') #garedelyon.getHeating() res=0 onoffcolor='blue' # legacy code - will be reinstated some day if onoff=='on': onoffcolor='red' import os,string ################### # make the adjust temperature button panel # this is dynamically constructed to show the current aiming temperature. buttonColours=['blue','#10e','#20d','#40b','#609','#807', '#a05','#b04','#c03','#d02','#e01','red'] aimIndex=math.trunc(aimtemp+0.5)-12 if aimtemp<=12.5: aimIndex=0 elif aimtemp>=22.5: aimIndex=11 buttonColours[aimIndex]='yellow' adjustPanel=''' <td><button name="button" value="" type="submit"></button></td> <td bgcolor="%s"> <button name="button" value="cooler" type="submit">COOLER</button> </td> <td bgcolor="%s"><button name="button" value="13" type="submit">13C</button></td> <td bgcolor="%s"><button name="button" value="14" type="submit">14C</button></td> <td bgcolor="%s"><button name="button" value="15" type="submit">15C</button></td> <td bgcolor="%s"><button name="button" value="16" type="submit">16C</button></td> <td bgcolor="%s"><button name="button" value="17" type="submit">17C</button></td> <td bgcolor="%s"><button name="button" value="18" type="submit">18C</button></td> <td bgcolor="%s"><button name="button" value="19" type="submit">19C</button></td> <td bgcolor="%s"><button name="button" value="20" type="submit">20C</button></td> <td bgcolor="%s"><button name="button" value="21" type="submit">21C</button></td> <td bgcolor="%s"><button name="button" value="22" type="submit">22C</button></td> <td bgcolor="%s"> <button name="button" value="hotter" type="submit">HOTTER</button> </td> ''' % (tuple(buttonColours)) ################### Relay Control <HouseMade: Relay Control 14.8> ################### Water Storage tanksection='' # tank([]) ################### Solar Power solarsection='' # solar([]) ################### Climate weathersection='' # weather([]) ############################## ###################
Chunk referenced in 14.3

14.2.6 The Relay Information section

<HouseMade: Relay Control 14.8> =
RelayServerGood=True if RelayServerGood: row="<tr><th>Name</th><th>bit No</th><th>On/Off</th>" row+="<th>Timer</th><th colspan='8'>Run For</th></tr>\n" for key in sorted(RelayTable, key=RelayTable.get): bitNo=RelayTable[key] thisBit=RelayState[bitNo] thisState=['off','on'][thisBit] newState=['on','off'][thisBit] thisColour=['lightblue','red'][thisBit] row+=' <form action="http://{}/~ajh/cgi-bin/" 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 14.7

14.2.7 define the Generate Weather Data routine

<HouseMade: define the Generate Weather Data routine 14.9> =
def weather(args): WServer='http://%s:5000/weather' % (CENTRAL) MServer='http://%s:5000/house' % (CENTRAL) MAXMINFILE='/home/ajh/logs/terang/maxmintemps.log' curtemp = w.inside.temp curhumid = w.inside.humidity outtemp = w.outside.temp outhumid = w.outside.humidity 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: max=float( min=float( 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=""> <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 14.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.

14.2.8 define the Generate Solar Data routine

<HouseMade: define the Generate Solar Data routine 14.10> =
def solar(args): SServer='http://%s:5000/solar' % (CENTRAL) MServer='http://%s:5000/house' % (CENTRAL) in_Ah = 0.0 # Central.getSolar(20) out_Ah = 0.0 # Central.getSolar(24) solaramps = 0.0 # Central.getSolar(32) solarbatteryvolts = 0.0 # Central.getSolar(34)*0.1+15 solarpower = 0.0 # solaramps*solarbatteryvolts percentsolar = 0.0 # solaramps * 100.0 / 50.0 in_whr = int(in_Ah*27.6) in_MJ = (in_Ah*27.6*3.6/1000) solarsection=""" <h2><a href="%(SServer)s">Solar power</a></h2> <image src="personal/solarplot.png"/> <p> We are getting %(solaramps).1fA into our %(solarbatteryvolts).1fV batteries for a total power output of %(solarpower).1fW, or about %(percentsolar).1f%% of maximum rated power.<br/> Today we've had %(in_Ah)dAh in (%(in_whr)dWhr=%(in_MJ).1fMJ) and %(out_Ah)dAh out. Note that the ampere-hours out is quoted for the 24vdc level, not the 240vac being generated by the inverter.<br/> There is a <a href="">detailed log</a> available, and the <a href="" 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 14.2

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

14.2.9 define the Generate Tank Data routine

<HouseMade: define the Generate Tank Data section 14.11> =
def tank(args): TServer='http://%s:5000/tank' % (CENTRAL) MServer='http://%s:5000/house' % (CENTRAL) # get tank data tankvolume=0.0 # RelayServer.getTank() tanktemp=volts=0.0 tankfull=4500 tankpercent = (tankvolume/float(tankfull))*100 tanksection=""" <h2><a href="%(TServer)s">Tank Storage</a></h2> <image src="personal/tankplot.png"/> <p> The rain water tank is currently at %(tankvolume).1fl(/%(tankfull)dl) = %(tankpercent).1f%%. Check the 7 day graph: </p> <image src="personal/tankplot7.png"/> <p><a href="%(MServer)s">Back to House</a></p> """ % vars() return tanksection
Chunk referenced in 14.2

This section now installed on lilydale.

14.2.10 Collect Date and Time Data

<HouseMade: collect date and time data 14.12> =
# collect date and time data (year, month, day, hour, minute, second, weekday, yday, DST) = time.localtime(time.time()) #tm = time.asctime(time.localtime(time.time())) + \ # ["", "(Daylight savings)"][DST] tm=tm.strftime("%a, %d %b %Y, %H:%M:%S") 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 14.3 14.16

The date and time at which this program is run is useful for logging, so collect it at the start of operations. The variable jobtime is intended to check how intensive use of this code may become.

14.2.11 Check the Client Connection

<HouseMade: check client connection 14.13> =
if 'SSH_CONNECTION' in os.environ: clientIP=os.environ['SSH_CONNECTION'] res=re.match('^(\d+\.\d+\.\d+\.\d+).*$',clientIP) if res: else: clientIP='' if DEBUG: print(os.environ) print(clientIP) clientIP='' 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 14.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.

14.2.12 Generate the Web Page Content

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

14.2.13 Make the Temperature Panel (not currently used)

<house make temperature panel 14.15> =
# make the adjust temperature button panel # this is dynamically constructed to show the current aiming temperature. buttonColours=['blue','#10e','#20d','#40b','#609','#807', '#a05','#b04','#c03','#d02','#e01','red'] aimIndex=math.trunc(aimtemp+0.5)-12 if aimtemp<=12.5: aimIndex=0 elif aimtemp>=22.5: aimIndex=11 buttonColours[aimIndex]='yellow' adjustPanel=''' <td><button name="button" value="" type="submit"></button></td> <td bgcolor="%s"> <button name="button" value="cooler" type="submit">COOLER</button> </td> <td bgcolor="%s"><button name="button" value="13" type="submit">13C</button></td> <td bgcolor="%s"><button name="button" value="14" type="submit">14C</button></td> <td bgcolor="%s"><button name="button" value="15" type="submit">15C</button></td> <td bgcolor="%s"><button name="button" value="16" type="submit">16C</button></td> <td bgcolor="%s"><button name="button" value="17" type="submit">17C</button></td> <td bgcolor="%s"><button name="button" value="18" type="submit">18C</button></td> <td bgcolor="%s"><button name="button" value="19" type="submit">19C</button></td> <td bgcolor="%s"><button name="button" value="20" type="submit">20C</button></td> <td bgcolor="%s"><button name="button" value="21" type="submit">21C</button></td> <td bgcolor="%s"><button name="button" value="22" type="submit">22C</button></td> <td bgcolor="%s"> <button name="button" value="hotter" type="submit">HOTTER</button> </td> ''' % (tuple(buttonColours))

This code is separated out because of the complexity of loading the colours of each of the demand buttons. Each button gets a graduated colour from blue through to red, except for the currently specified temperature, which is shown with a yellow background. Temperatures are rounded to the nearest integer in order to determine which button is so highlighted. Temperatures above and below the selectable range highlight the HOTTER and COOLER buttons respectively.

There is a slight glitch with the operation of this form, in that when no specific temperature button is selected (such as when a text value of temperature is entered, the first button is selected (which would normally be COOLER). (See <house get calling parameters >.) To avoid this, a dummy blank button (value="") is built in at the start of the table list, and when this blank value is recognized, the text value is used instead.

14.3 The HeatingModule module

This web page provides a user-friendly interace to setting the automatic heating on/off times, and the temperatures over the course of a day (this latter function not yet operational). The week is divided into 7 days, each with its own programme.

Each day can have up to 7 blocks of time, where the start time of the first block is midnight (00:00 hours), and the end time of the last block is the next midnight (24:00 hours). The end time of each block can be altered, and the number of blocks is determined by the block that has end time of 24:00.

On saving, if the last end time is earlier than 24:00, a new block is added (up to 5 blocks). If seven blocks are in use, and the last end time does not end at midnight, the program will be incomplete and the actual behaviour is not defined.

The desired temperature of each time block can be set independently.

"" 14.16 = **** File not generated!
#! /usr/bin/python ## ********************************************************** ## * do NOT EDIT THIS FILE! * ## * Use $HOME/Computers/House/HouseMade.xlp instead * ## ********************************************************** ## ## 20141113:114917 1.0.0 ajh first version with number ## 20141113:114958 1.0.1 ajh elide start times if narrow column ## 20150722:164226 1.1.0 ajh copied from TimerModule and updated ## import cgi,datetime,math,os,sys,re,time from HouseDefinitions import * DEBUG=False <Web: define the heatingData class 14.17> def heating(logMsg,remadr,args): DEBUG=False active=False <HouseMade: collect date and time data 14.12> environ=os.environ if DEBUG: keys=environ.keys() keys.sort() print("environ:") for key in keys: print(" %s:%s" % (key,environ[key])) if DEBUG: keys=args.keys() keys.sort() print("arguments",) lastKey='' for key in keys: if key[0:3]!=lastKey: print("\n ",) lastKey=key[0:3] print("%s:%s" % (key,args[key]),) print("\n\n", # put 2 newlines at end) server=CENTRAL #print("server=%s" % (server)) clientIP=remadr res=re.match('10\.0',clientIP) if res: clientOK=True else: res=re.match('130\.194\.69\.41',clientIP) if res: clientOK=True else: clientOK=False logMsg("clientOK=%s (%s)" % (clientOK,clientIP)) # create data structures and initialize td=heatingData() # load previously saved data td.load('/home/ajh/Computers/House/heatProgram.dat') <Web: heating: collect parameters and update 14.18> <Web: heating: build widths for web page table 14.19> # build web page redirect='' if active: redirect='''<meta http-equiv="Refresh" content="10;URL='%s'>''' % (HServer) out="<HTML>\n<HEAD>\n" out+=redirect out+='<meta http-equiv="Pragma" content="no-cache">\n' out+='<TITLE>HeatingTimer</TITLE>\n' out+="weekday=%d, now=%s" % (weekday,now) if not clientOK: out += "<P>Sorry, you are not authorized to adjust this table</P>" else: out += '<form action="%s" method="get" name="heating">\n' % (HServer) out += ' <button name="button" value="save">save</button>\n' out += ' <table border="1" width="100%" padding="0">\n' for i in range(7): out += " <tr height='40px'>\n" if i==weekday: dayColour="#8f8" else: dayColour="#fff" out += " <td width='10%%' bgcolor='%s'>%s</td>\n" % (dayColour,td.days[i]) out += " <td><table width='100%' height='100%' border='0' padding='0' cellspacing='0'><tr>\n" for j in range(NTempBlocks): if td.width[i][j]==0: continue out += " <td bgcolor='%s' width='%d%%' height='35px' align='center'>\n" % (td.colour[i][j],td.width[i][j]) out += ' <select name="temp-%d-%d" size="1">\n' % (i,j) for k in range(10,27): selected="" if k==td.temp[i][j]: selected="selected" out += ' <option value="%d" %s>%d</option>\n' % (k,selected,k) out += ' </select>\n' out += " </td>\n" out += " <td> </td>\n" out += " </tr>\n" out += " <tr>\n" for j in range(NTempBlocks): if td.width[i][j]==0: continue out += " <td bgcolor='%s' width='%d%%' height='35px' align='center'>\n" % (td.colour[i][j],td.width[i][j]) out += " <table border='0'>\n" (sh,sm)=td.mins2Hours(td.start[i][j]) (eh,em)=td.mins2Hours(td.end[i][j]) if td.width[i][j]>20: out += ' <tr><th>Start</th><th>End</th></tr>\n' out += ' <tr><td>%02d:%02d</td>\n' % (sh,sm) else: out += ' <tr><th>End</th></tr>\n' out += ' <tr>\n' out += ' <td>\n' out += ' <select name="end-%d-%d" size="1">\n' % (i,j) for k in range(0,25): selected="" if k==eh: selected="selected" out += ' <option value="%02d" %s>%02d</option>\n' % (k,selected,k) out += ' </select>\n' out += ' <select name="endmin-%d-%d" size="1">\n' % (i,j) for k in range(0,12): selected="" if 5*k==em: selected="selected" out += ' <option value="%02d" %s>%02d</option>\n' % (5*k,selected,5*k) out += ' </select>\n' out += ' </td>\n' out += ' </tr>\n' out += ' <tr>\n' out += ' <td>\n' out += ' </td>\n' out += ' </tr>\n' out += " </table>\n" out += " </td>\n" out += " <td> </td>\n" out += " </tr></table></td>\n" out += " </tr>\n" out += " </table>\n" out += '</form>\n' out += '<A HREF="http://%s:5000/house">back to house</A>\n' % (CENTRAL) if clientOK:'/home/ajh/Computers/House/heatProgram.dat') print("--------") return out if __name__=='__main__': heating()

14.3.1 Define the heatingData Class

<Web: define the heatingData class 14.17> =
class heatingData(): def __init__(self): self.days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'] self.temp=[[ThermostatSetting for j in range(NTempBlocks)] for i in range(7)] self.start=[[0 for j in range(NTempBlocks)] for i in range(7)] self.end=[[0 for j in range(NTempBlocks)] for i in range(7)] self.width=[[10 for j in range(NTempBlocks)] for i in range(7)] self.colour=[['red' for i in range(NTempBlocks)] for j in range(7)] def mins2Hours(self,m): return (m/60,m%60) def hours2Mins(self,h,m): return 60*h+m def load(self,filename='/home/ajh/Computers/House/heatProgram.dat'): f=open(filename,'r') for i in range(7): day=f.readline() res=re.match('Day (\d)$',day) if res: rd=int( 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( s=60*int( e=60*int( t=int( self.temp[i][j]=t self.start[i][j]=s self.end[i][j]=e self.colour[i][j]=setColour(self.temp[i][j]) if n!=j: print("Error on block %d on day %d" % (j,i)) else: break; if block.strip()!='': blank=f.readline() f.close() def save(self,filename='/home/ajh/Computers/House/heatProgram.dat'): f=open(filename,'w') for i in range(7): f.write("Day %d\n" % (i)) for j in range(NTempBlocks): s=self.start[i][j] sh=s/60; sm=s%60 e=self.end[i][j] eh=e/60; em=e%60 t=self.temp[i][j] f.write("%1d %02d%02d-%02d%02d:%02d\n" % (j,sh,sm,eh,em,t)) f.write("\n") pass f.close()
Chunk referenced in 14.16

This class deals with all the logic needed to load and save the heating data, stored in a separate file. It handles conversion from external stored time data in hours:minutes format, converting it to internally stored minutes only (from the start of the day), and reconverting it back again on saving.

It also provides a few simple conversion routines for switching between the formats.

14.3.2 Collect Parameters and Update

<Web: heating: collect parameters and update 14.18> =
# collect parameters if clientOK: if args: active='True' keys=args.keys() keys.sort() for k in keys: #print("k=%s" % (k)) res=re.match('(temp|start|end|endmin)-(\d+)-(\d+)',k) if res: d=int( b=int( #print("got type=%s, day=%d, block=%d" % (t,d,b)) if t=='temp': tt=args[k][0] #print("d=%s, b=%s, temp=%s, args[%s]=%s" % (d,b,tt,k,args[k])) t=setTemperature(args[k][0]) td.temp[d][b]=t td.colour[d][b]=setColour(t) pass # 'start' is never used #if t=='start': # #print("<p>",temp,k,args[k]) # start[d][b]=int(args[k][0]) # pass if t=='end': #print("<p>",temp,k,args[k]) (h,m)=td.mins2Hours(td.end[d][b]) td.end[d][b]=td.hours2Mins(int(args[k][0]),m) pass if t=='endmin': #print("<p>",temp,k,args[k]) (h,m)=td.mins2Hours(td.end[d][b]) if h>=24: m=0 else: m=int(args[k][0]) td.end[d][b]=td.hours2Mins(h,m) pass
Chunk referenced in 14.16

14.3.3 Build Widths for Web Page Table

<Web: heating: build widths for web page table 14.19> =
# build widths for table for i in range(7): dayFinished=False for j in range(NTempBlocks): if j>0: # make unused blocks alternate in temperature if td.start[i][j]==1440: # 1440 is midnight, hence unused if td.temp[i][j-1]==10: td.temp[i][j]=ThermostatSetting else: td.temp[i][j]=10 try: td.start[i][j]=td.end[i][j-1] except IndexError: print("index error in HeatingModule: i=%d, j=%d (start=%s, end=%s)" \ % (i,j,td.start,td.end)) if td.start[i][j]>td.end[i][j]: td.end[i][j]=60*24 w=td.end[i][j]-td.start[i][j] if w<0: w=0 if dayFinished: w=0 td.width[i][j]=math.trunc(100*w/1440.0) # percentage width (h,m)=td.mins2Hours(td.end[i][j]) if h==24 or h==0: dayFinished=True #print("got day finished at day=%d, block=%d" % (i,j)) pass
Chunk referenced in 14.16
<check client IP address OK 14.20> =
if 'REMOTE_ADDR' in os.environ: clientIP=os.environ['REMOTE_ADDR'] else: clientIP='' print(os.environ) res=re.match('10\.0',clientIP) if res: clientOK=True else: res=re.match('130\.194\.69\.41',clientIP) if res: clientOK=True else: clientOK=False

This code fragment checks to see if the client IP address is on the local network, and sets clientOK to True if it is, False otherwise.

15. The Weather System

The heart of the weather system is an electronic weather station with RS232 output, that is continously monitored by the garedelyon server. Once a minute, this server logs the current inside and outside temperatures, humidity and dew points. This information is stored in a logfile, temp.log in the directory /logdisk/logs/, and accessed by the heating and web systems.

15.1 The C Weather Monitor Program

(Details to be recorded)

15.2 The Python Interface to the Weather System

This is a simple python module that accesses the monitor progam and makes the data accessible as python structures. There is one substantive class, weather, instances of which entities containing the appropriate data.

The null classes environment, wind, rain, and pressure provide further localization of the data values.

defines values for temperature, humidity, and dew point;
defines values for wind gust, wind gustdirirection, avgerage wind speed, avgdir average wind direction, and wind chill factor.

"" 15.1 = **** File not generated!
#!/usr/bin/python <edit warning 2.1> import os,string,sys,subprocess class environment: pass class wind: pass class rain: pass class pressure: pass class weather: def __init__(self, hostname = ""): 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]) = int(l[15]) if __name__ == '__main__': d = weather() for n in dir(d): a = getattr(d, n) for nn in dir(a): print("%s.%s." % (n, nn), str(getattr(a, nn)))

15.3 The Weather Logging Process

This code is run once a minute by the 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.

"" 15.2 = **** File not generated!
import datetime import re from wx200 import * STATEFILE='/home/ajh/logs/terang/wxState.txt' MAXMINFILE='/home/ajh/logs/terang/maxmintemps.log' w=weather() inside=w.inside intemp=inside.temp outside=w.outside wind=w.wind rain=w.rain 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( outside.temp=float( 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: max=float( min=float( maxmintable[d]=(max,maxat,min,minat) else: print("cannot parse %s" % (l)) f.close() changed=False today=now.strftime("%Y%m%d") time=now.strftime("%H:%M") if today in maxmintable: (max,maxat,min,minat)=maxmintable[today] else: maxmintable[today]=(outside.temp,time,outside.temp,time) changed=True (max,maxat,min,minat)=maxmintable[today] if outside.temp>max: max=outside.temp maxat=time changed=True elif outside.temp<min: min=outside.temp minat=time changed=True if changed: maxmintable[today]=(max,maxat,min,minat) f=open(MAXMINFILE,'w') keys=maxmintable.keys() keys.sort() for k in keys: (max,maxat,min,minat)=maxmintable[k] f.write("%s %5.1f %s %5.1f %s\n" % (k,max,maxat,min,minat)) #print("%s %5.1f %s %5.1f %s" % (k,max,maxat,min,minat)) f.close()

16. The Heating System

16.1 AdjustHeat

This script runs every minute on lilydale, to see if the heating should be adjusted. An entry in 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 < >.

"" 16.1 = **** File not generated!
<edit warning 2.1> # this code must run on lilydale import datetime import re import RelayControl import xmlrpclib import HeatingModule import currentState from HouseDefinitions import * import wx200 Debug=False hysteresis=0.25 nowStamp=now.strftime("%Y%m%d:%H%M%S") dayofweek=now.isoweekday() % 7 hd=HeatingModule.heatingData() hd.load('/home/ajh/Computers/House/heatProgram.dat') 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()'thermostat',desiredTemp)

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.


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

"" 16.2 = **** File not generated!
#! /usr/bin/python <edit warning 2.1> import re import datetime import xmlrpclib 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( 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( start[i][j]=int( end[i][j]=int( temp[i][j]=int( 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, and adjust the desired temperature to match the programmed temperature.

The key requirement to this logic is that changes should only be made at the appointed switch time.

There seems to be some sort of race condition in the switching logic. The trailing message has been added to try and identify the circumstances under which the planned temperature and desired temperature disagree.

16.3 TempLog

This script logs the house temperature. It runs every minute to maintain a minute-by-minute log. It must be run on garedelyon, since that is where all logging is collected (the log file temp.log is kept in the directory /logdisk/logs).

"" 16.3 = **** File not generated!
<edit warning 2.1> # this code must run on garedelyon import os,string,datetime logfile="/logdisk/logs/temp.log" nowstr=now.strftime("%Y%m%d:%H%M%S") log=open(logfile,'a') hostname='' cmd="/home/ajh/Computers/House/wx200d-1.1/wx200 %s -l --nounits" % hostname f = os.popen(cmd) line = f.readline() l = map(lambda s:string.strip(s[:-1]), string.split(line, "\t")) intemp=float(l[0]) outtemp=float(l[1]) inhumd=float(l[2]) outhumd=float(l[3]) outdew=float(l[5]) currheat=open('/logdisk/logs/heating','r') ch=currheat.readline() therm=float(ch) onoff=currheat.readline().strip() fmt="%s %5.1f %5.1f %5.1f %5.1f %5.2f %5.1f %s\n" vars=(nowstr,intemp,outtemp,inhumd,outhumd,outdew,therm,onoff) log.write(fmt % vars)

17. The Tank System

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

17.1 Water Tank Logging

The water logging code has been re-written from C to Python, to bring it in line with the rest of the system, and to (hopefully) improve the reliability of the logging. The Python code has been added to the existing code to manage the tank system, namely

17.2 Start the Tank Logging

"" 17.1 = **** File not generated!
<edit warning 2.1> LOGDIR='/home/ajh/logs/terang' HOUSE='/home/ajh/Computers/House' USB=`getDevice tank` kill -9 `cat ${LOGDIR}/tankProcess` rm ${LOGDIR}/tankProcess python $USB & ps aux | grep "python" | 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 program.

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

17.3 Tank Module Functions

The tank module 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, 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.

"" 17.2 = **** File not generated!
#!/usr/bin/python <edit warning 2.1> import datetime import getopt import re import string import sys import time import os import usbFind LOGFILENAME="/home/ajh/logs/terang/tank.log" maxlitres = 2250 # for one tank minlitres = 0 # actual somewhat more maxcap = 68750 # capacitance reading for full supply level mincap = 11484 # capacitance reading when tank is empty NumberOfTanks=2 if NumberOfTanks>1: maxlitres *= NumberOfTanks minlitres *= NumberOfTanks slope=(maxlitres-minlitres)/float(maxcap-mincap) base=minlitres-slope*mincap def compensate(level,temp,ntanks=NumberOfTanks): ''' returns a temperature compensated tank level (NOT litres)''' level=level+213*(temp-28.2)*(level/73035.0) return level def convert(level): '''returns the (uncompensated) volume corresponding to the capacitance meter output value @level. ''' litres=base+slope*float(level) return litres def read_tank_state(): f=open('/home/ajh/logs/terang/tankState','r') l=f.readline() if len(l) <= 1: return l = string.split(l) if len(l) < 3: return (level, temp) = map(int, l[1:3]); return (level, temp) def calibrated(): tankdepth,tanktemp=read_tank_state() bigtankheight=1450 tankdepth = (float(tankdepth-mincap)/float(maxcap-mincap))*bigtankheight volume = compensate(tankdepth,tanktemp) return (volume,tanktemp) def readraw(dev):,os.O_RDWR) res='' while not res:,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( volts=int( 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.

18. The Solar System

This is almost verbatim from the central version, although some modifications have been made to correct what appeared to be the presence of obsolete code in read_register.

This code is responsible for building the solar.log file. It is to be run every minute on garedelyon (c.f., and makes calls upon the pl60 module (a C program) to read the solar data from the solar controller, register by register (Need a link to the solar controller manual here). The log entry is then printed to standard output for logging, and to the file solarState which records the most recent value.

These are the registers of the pl60:
17 Battery Vmax
20 Charge AH
24 Load AH
32 Input Current
34 Battery Voltage
Note that the values returned by these registers (may) need recalibration. Others not mentioned are not used.


"" 18.1 = **** 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 cron script.


Provide definitions and access functions for the solar controller.

"" 18.2 = **** File not generated!
#!/usr/bin/python <edit warning 2.1> import cgi import os import string import time def read_register(i): f = os.popen("pl60 -b %d" % i, "r") line=f.readline() out = int(line) f.close() return out def int_register(i): return int(read_register(i)) def float_register(i): val=float(read_register(i)) if i==32: val=0.4*val return val def main(): in_Ah = int_register(20) out_Ah = int_register(24) solaramps = int_register(32)*0.4 solarbatteryvolts = int_register(34)*0.1+15 solarpower = solaramps*solarbatteryvolts percentsolar = solaramps * 100.0 / 43.0 t = time.time() dt = time.strftime("%Y%m%d") tm = time.strftime("%H:%M") WHcost = 0.00013 numPanels = 20 data_Panel = 0.0 print("in_Ah=%4.1f, out_Ah=%4.1f, solaramps=%4.1f" %\ (in_Ah, out_Ah, solaramps)) if __name__ == "__main__": main()

Note that this code is intended to be imported as a module to python programs that need access to the solar controller. It may be called as a stand-alone program, when it simply prints key data and exits.

19. The House Computer

19.1 The Current State Interface

"" 19.1 = **** File not generated!
#!/usr/bin/python Debug=False STATEFILE='/home/ajh/logs/terang/houseState' class HouseState(): def __init__(self): state={} def load(self,fname=STATEFILE): f=open(fname,'r') 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')) if __name__=='__main__': main()

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

19.2 The HouseData Server (obsolete)

Defines an RPC server to provide details of the current house state (e.g., the contents of the heating file, the log files, etc.), and to update data as required..

Most of this code was stolen from the Python Library Reference document, and revised from the SimpleHeatExchanger.

This code has been decommissioned, as the server (garedelyon) is defunct. All of these functions are now available through the main house server (lilydale).

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

Note that the water log should be moved from the root directory to the /logdisk/logs directory to be consistent. Also, we should find a way to manage the bound on the size of the log files (getting the maximum is O(n), for example, whereas it should be O(hours)).

19.2.1 HouseData define getTemps

<HouseData define getTemps 19.3> =
# define the get water level function def getTemps(): logfile='/logdisk/logs/temp.log' cmd=['/usr/bin/tail','-1',logfile] pipe=subprocess.Popen(cmd,stdout=subprocess.PIPE) p=pipe.stdout l=p.readline() p.close() print(l) res=temppat.match(l) if res: intemp=float( outtemp=float( else: intemp=0.0 outtemp=20.0 return (intemp,outtemp)
Chunk referenced in 19.2

19.2.2 HouseData define maxminTemp

<HouseData define maxminTemp 19.4> =
# Define the max and min temperature function # returns a table of maxima and minima, computed previously def maxminTemp(): maxmintable={} logfile='/logdisk/logs/maxmins.log' f=open(logfile,'r') for l in f.readlines(): res=maxminpat.match(l) if res: maxmintable[d]=(max,maxat,min,minat) f.close() return maxmintable
Chunk referenced in 19.2

19.3 The script

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

20. The Event Manager

This section has been superceded by the EventScheduler module, and has its code sections commented out.

This was a new section in version 2, and represented a change in thinking on how best to do the timing of events in the house environment. One factor is the ability to provide trace-back analysis when things go wrong, and to maintain a permanent record of what has been happening. This means migrating a significant amount of data into files, rather than hard-coding it into programs and program structures.

The model is this:

  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.

"" 20.1 =
#!/home/ajh/binln/python3 import datetime import getopt import time import re import sys import ChookDoor import GardenSteps import GardenWater from HouseDefinitions import * debug=0 ; testing=0 ; fastmode=0 verbose=0 NEFname="/home/ajh/Computers/House/events.txt" dayp ="(Sun\w*|Mon\w*|Tue\w*|Wed\w*|Thu\w*|Fri\w*|Sat\w*|\*) +" datep="(|\d* +|{})".format(dayp) # groups 1,2 timep="(\d+:\d+)(@\d+|-\d+:\d+)?" # groups 3,4 object="(\w+)" # groups 5 parms="(\(.*\))" # groups 7 list="([\w ]*)" # groups 8 eventPat="{}{} +{}({}|{})".format(datep,timep,object,parms,list) # groups 6 chookdoor=ChookDoor.ChookDoor() gardensteps=GardenSteps1.GardenSteps() gardenwater=GardenWater1.GardenWater() def strEvent(ev): if not ev: return 'None' (d,t,o,p,h)=ev s='' s+="{} ".format(d) s+="{} ".format(t) s+="{} ".format(o) s+="{} ".format(p) s+="{} ".format(h) return s <Event Manager: Event class 20.2> <Event Manager: Clock 20.3> <Event Manager: class eventManager 20.5> <Event Manager: main 20.4> if __name__=='__main__': (vals,path)=getopt.getopt(sys.argv[1:],'dftvV', ['debug','fast','testing','verbose','version']) for (opt,val) in vals: if opt=='-d' or opt=='--debug': debug=1 ; testing=1 ; fastmode=1 if opt=='-f' or opt=='--fastmode': fastmode=1 if opt=='-t' or opt=='--testing': testing=1 if opt=='-v' or opt=='--verbose': verbose=1 if opt=='-V' or opt=='--version': print(version) sys.exit(0) if len(path)!=0: usage() sys.exit(1) main(testing,fastmode)

The eventManger module provides a class to do all the management of events in the house system. It no longer maintains a file of saved events.

Thinking aloud bit: I have generated a bit of self confusion over what modes of operation should be implemented. Here is a table of what should be happening:

No Testing Testing, CLI "-t"
No fast mode Normal operation, normal speed Testing operation, normal speed
Fast mode, CLI "-f" Normal operation, fast speed Testing operation, fast speed (CLI "-d")

Because debugging is the more commonly thought-of mode of testing, it is actually a shorthand for setting both the testing and fastmode simultaneously. It is (currently) unused otherwise, but could be used in future to control other debug information.

20.1 The Event class

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

20.2 The Clock

<Event Manager: Clock 20.3> =
def Clock(em,fastmode=False): if fastmode: print("Clock is running in fastmode") interval=60 nowTime=now.strftime("%H:%M") needEvent=True while True: 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 20.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.

20.3 Event Manager: main

<Event Manager: main 20.4> =
def main(testing=False,fastmode=False): nowtime=now.time() e=eventManager() for ev in e.eventsList: print(strEvent(ev)) order=e.sortEvents() e.eventsList=order,testing),testing),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 20.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.

20.4 The Event Manager class

<Event Manager: class eventManager 20.5> =
class eventManager(): def __init__(self): self.fn=NEFname self.eventsList=[] self.isordered=False self.nextEvent=0 return <Event Manager: sort events 20.6> <Event Manager: get next event 20.7> <Event Manager: handle event 20.8> <Event Manager: register event 20.9>
Chunk referenced in 20.1

20.4.1 the sortEvents method

<Event Manager: sort events 20.6> =
def sortEvents(self): def sortkey(ev): return ev[1] evlist=self.eventsList evlist.sort(key=sortkey) self.isordered=True return evlist
Chunk referenced in 20.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.

20.4.2 the getNextEvent method

<Event Manager: get next event 20.7> =
def getNextEvent(self): # comment out the next line for testing 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 20.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.

20.4.3 Handle an event

<Event Manager: handle event 20.8> =
def handleEvent(self,ev): #print(strEvent(ev)) (d,t,o,p,h)=ev key="{}".format(o.strip()) if h: print("Now handling event {}".format(strEvent(ev))) h(p) else: print("Unregistered action {} at {}".format(key,t))
Chunk referenced in 20.5

20.4.4 Register an Event

<Event Manager: register event 20.9> =
def registerEvent(self,ev,handle): (d,eventTime,eventName,eventParms,h)=ev if not self.isordered: order=self.sortEvents() self.eventsList=order # now scan list in chronological order for i in range(len(self.eventsList)): evn=self.eventsList[i] (d,t,o,p,h)=evn #print("checking register time {} against {}".format(eventTime,t)) if eventTime == t: #print("Have two events at the same time {}/{}".format(eventTime,t)) #print("Event names are {}/{}".format(eventName,o)) # check if duplicate event if o == eventName: # is the same, replace old event at this time #print("register {} at same time as {}".format(strEvent(ev),strEvent(evn))) self.eventsList[i]=('*',eventTime,eventName,eventParms,handle) return elif eventTime < t: # event goes in list before this element #print("Event {} entered before {}".format(strEvent(ev),strEvent(evn))) self.eventsList.insert(i,('*',eventTime,eventName,eventParms,handle)) return # reached the end, insert here #print("Event {} entered at end".format(strEvent(ev))) self.eventsList.append(('*',eventTime,eventName,eventParms,handle))
Chunk referenced in 20.5

21. Test Programs

21.1 Check RPC Operation

The following short fragment of code is intended to check the operation of the RPC mechanisms on both garedelyon and lilydale. It provides the user with one RPC object, o (bastille), which can be used to invoke the RPC interfaces. Several such (information supply only) interfaces are invoked as examples.

Usage is to import this code into an interpretive invocation of python, viz from testRPC import *.

"" 21.1 =
<edit warning 2.1> import xmlrpc.client import HouseDefinitions RelayServer=HouseDefinitions.RelayServer print("options are:") print(" RelayServer.getState()") print(" RelayServer.setState([0,0,...]) # 12-element vector of 0/1") print(" RelayServer.setBitOn(bitnumber) # bit number is an integer (0-11)") print(" RelayServer.setBitOff(bitnumber) # bit number is an integer (0-11)") print() #print(" RelayServer.getHeating()") #print(" RelayServer.setHeating(float,'on'/'off')") #print(" RelayServer.getSolar(n) # n is register number (32 is input amps)") #print(" RelayServer.getTemps()") #print(" RelayServer.getTank()") #print(" RelayServer.maxminTemp()") #print() #print("for example,") #print(" RelayServer.getHeating()=%s" % (RelayServer.getHeating())) #print(" RelayServer.getState()=%s" % (s.getState())) print

Note that the data logging operations are not currently available.

22. The Log Files

Here is a summary of all the log files maintained:

lilydale:/Users/ajh/logs/RelayServer.log logs activity of the relay controller, recording relay sets and resets.
bastille:/home/ajh/Computers/House/cron.log logs behaviour of the 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)
garedelyon:/logdisk/logs/housedata.log logs calls on the garedelyon RPC server, along with some debugging information (which should probably be removed).
garedelyon:/logdisk/logs/maxmins.log logs the maximum and minimum outside temperatures for the last 8 days.

23. Installing and Starting the HouseMade Software

23.1 Introduction

This has always been a somewhat fraught area of development since the earliest versions of this software. That is largely due to the variety of both hardware and software in use, and the various idiosyncracies involved. This section attempts to address these issues.

This is a list of all the processes that need starting:

  1. The Beagle subsystem software, involving a relay driver, a relay/chook door state request server BeagleServer, and the BeagleBone GPIO setup AJH-GPIO-Relay.dts.
  2. The House Data Logging Computer relay interface
  3. chook door (the need for this as a standalone is currently in question).

23.2 Details

23.2.1 Start the Beagle Server

The BeagleServer (aka the chook door server) provides an RPC interface to a) interrogate whether or not the chook door is closed, according to the proving microswitch, and b) drive the relay switching.

The proving circuits are independent of the actual closing and opening process, and provides a safety check that the door is actually closed, once the close command has been issued, and the door has had time to close. It runs continuously on a BeagleBone server (kerang), and can be started with the make call:

            make start-beagle

This make call just invokes the following script (after making sure that its code is up-to-date) on the BeagleBone (known by its network name kerang). The script can also be invoked directly on the kerang machine from the command line.

"" 23.1 =
#!/bin/bash LOGDIR='/home/ajh/logs/kerang' HOUSE='/home/ajh/Computers/House' BIN=${HOME}/bin # collect any previous instance ps aux | grep "" | 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/ >>~/logs/kerang/BeagleServer.log & # record the new instance ps aux | grep "" | 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/
will run the server in that window. Note that as this program runs continuously, it should be started in a separate terminal window (which can then hidden from view but left running). This mode has the advantage that any output from the server can be seen immediately in that window, rather than having to examine the logfile (as required by the start Beagle methods above).

23.2.2 Relay Server

The RelayServer runs continously on the House Data Logging computer (currently set as terang. It can be started with a make call:

            make start-relayserver

If this doesn't work, or you need a more direct interface, then

            (any machine) $ ssh terang /home/ajh/Computers/House/
Since this program runs continuously, it should be started in a separate terminal window, which is then hidden from view, but left running.

24. The Cron Jobs

Currently no cron jobs are required.

25. Makefile

"Makefile" 25.1 =
default=HouseMade CGI = ${HOME}/www/cgi-bin HOUSE = ${HOME}/Computers/House BIN = ${HOME}/bin CENTRAL = terang CENTRALHOUSE = $(CENTRAL):$(HOME)/Computers/House NEWPORT = newport.local NEWPORTHOUSE = $(NEWPORT):$(HOME)/Computers/House SPENCER = spencer.local SPENCERHOUSE = $(SPENCER):$(HOME)/Computers/House OUYEN = ouyen.local OUYENHOUSE = $(OUYEN):$(HOME)/Computers/House TERANG = terang TERANGHOUSE = $(TERANG):$(HOME)/Computers/House XSLLIB = /home/ajh/lib/xsl XSLFILES = $(XSLLIB)/lit2html.xsl $(XSLLIB)/tables2html.xsl WEBSOURCE = ${HOME}/www/computing/sources/house CGIFILES = \ SCP = /usr/bin/scp RSYNC = /usr/bin/rsync include ${HOME}/etc/MakeXLP GenFiles = \ \ \ \ ${GenFiles}:HouseMade.tangle install: install-central # install-central installs all executable files into the House directory # on CENTRAL, whatever that it install-central: make-executable install-eventedit install-events install-house @if [ $(CENTRAL) = $(NEWPORT) ] ; then \ rsync -auv $(CGIFILES) $(NEWPORT):/home/ajh/public_html/cgi-bin/ ; \ fi @if [ $(CENTRAL) = $(SPENCER) ] ; then \ rsync -auv $(CGIFILES) $(SPENCER):/home/ajh/public_html/cgi-bin/ ; \ fi @if [ $(CENTRAL) = $(TERANG) ] ; then \ rsync -auv $(CGIFILES) $(TERANG):/home/ajh/public_html/cgi-bin/ ; \ fi <Makefile: RelayServer makes 25.2> <Makefile: BeagleBone makes 25.3> make-executable: HouseMade.tangle chmod 755 chmod 755 chmod 755 install-eventedit: HouseMade.tangle chmod 755 cp -p /home/ajh/public_html/cgi-bin/ touch install-eventedit install-events: HouseMade.tangle ${RSYNC} ${CENTRAL}:/home/ajh/public_html/cgi-bin/ cp -p /home/ajh/public_html/cgi-bin/ touch install-events install-house: HouseMade.tangle ${RSYNC} ${CENTRAL}:/home/ajh/public_html/cgi-bin/ ${RSYNC} ${CENTRAL}:/home/ajh/public_html/cgi-bin/ ${RSYNC} ${CENTRAL}:/home/ajh/public_html/cgi-bin/ touch install-house HouseMade.tangle HouseMade.xml: HouseMade.xlp xsltproc --xinclude $(XSLLIB)/litprog.xsl HouseMade.xlp >HouseMade.xml touch HouseMade.tangle RelayControl.tangle HouseMade.html: HouseMade.xml $(XSLFILES) xsltproc --xinclude $(XSLLIB)/lit2html.xsl HouseMade.xml >HouseMade.html html: HouseMade.html pdf: HouseMade.pdf ########################### # MAKEFILE # ########################### makefile: Makefile.tangle ############################# # END OF INSTALLATION STUFF # ############################# executable: chmod 755 all: HouseMade.html clean: litclean -rm $(GenFiles)

25.1 RelayServer Makes

<Makefile: RelayServer makes 25.2> =
install-relayserver: HouseMade.tangle RelayControl.tangle make-executable $(SCP) $(CENTRALHOUSE)/ touch install-relayserver install-startRelay: HouseMade.tangle install-relay make-executable $(SCP) $(CENTRALHOUSE)/ touch install-startRelay # start the House Computer Relay Server start-relayserver: HouseMade.tangle install-startRelay ssh $(CENTRAL) $(HOME)/Computers/House/ touch start-relayserver
Chunk referenced in 25.1

25.2 BeagleBone Makes

<Makefile: BeagleBone makes 25.3> =
install-beagle: HouseMade.tangle chmod 755 chmod 755 $(SCP) $(TERANGHOUSE)/ $(SCP) $(TERANGHOUSE)/ ssh $(TERANG) mv $(HOUSE)/ $(HOME)/bin/ touch install-beagle start-beagle: HouseMade.tangle install-beagle ssh terang $(HOME)/bin/ touch start-beagle
Chunk referenced in 25.1

25.3 Makefile: install bastille

<Makefile: install bastille 25.4> =
############################## # INSTALL BASTILLE CODE # ############################## # install flask modules # keep these arranged alphabetically install-AdjustHeat: HouseMade.tangle $SCP ${BASTILLE}/ touch install-AdjustHeat install-arduino: RelayControl.tangle $SCP bastille:/home/ajh/bin/ $SCP ${BASTILLE}/ $SCP ${BASTILLE}/ touch install-arduino install-everyMinute: HouseMade.tangle chmod 755 $SCP ${BASTILLE}/ touch install-everyMinute install-html: HouseMade.html if /usr/bin/diff HouseMade.html ${WEBSOURCE}/HouseMade.html; then \ cp HouseMade.html ${WEBSOUCE}/HouseMade.html ;\ fi install-startRelay: HouseMade.tangle install-relay chmod 755 $SCP $(CENTRAL)/ touch install-startRelay install-tank: HouseMade.tangle $SCP ${BASTILLE}/ $SCP ${BASTILLE}/ $SCP ${BASTILLE}/ touch install-tank install-timer: HouseMade.tangle $SCP ${BASTILLE}/ touch install-timer start-relay: install-startRelay ssh bastille ${HOUSE}/ touch start-relay start-arduino: install-arduino ssh bastille ${HOUSE}/ touch start-arduino start-tank: install-tank ssh bastille ${HOUSE}/ touch start-tank

26. Document History

20200715:133241 ajh 2.0.0 Major transformation to deal with complete house renovation, along with the decommissioning of most of the system components. First task: get working.
20200716:103919 ajh 2.0.1 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, 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 will be left unused for now, to be removed at some stage in the future
20200926:095443 ajh 2.1.6 removed code for EventServer
20200927:150042 ajh 2.1.7 removed redundant code in getNextEvent
20201004:125117 ajh 2.1.8 tweaks to provide more logging information
20201011:181914 ajh 2.2.0 add garden water module, first attempt
20201207:112418 ajh 2.2.1 Changed ChookDoor, GardenSteps, GardenWater modules to be ChookDoor1, GardenSteps1, GardenWater1 to avoid conflict with revised system EventServer.
20201213:133903 ajh 3.0.0 merge of HouseMade 2.2.1 and EventServer 1.0.4, otherwise, little change
20201213:154451 ajh 3.0.1 migrated separate Makefile.xlp into this file
20201214:165523 ajh 3.0.2 Updated the History section.
20201221:131301 ajh 3.0.3 revisions in moving suite to Terang
20201229:133233 ajh 3.0.4 various tidy-ups of literate code
<current version 26.1> = 3.0.4
<current date 26.2> = 20201229:133233

27. Indices

27.1 Files

File Name Defined in
AJH-GPIO-Relay.dts 3.1 16.1 3.5 3.3 3.4 11.1 20.1 10.1 8.3 5.1 12.1 13.1 14.16 19.2 2.2 14.2
Makefile 25.1 4.17 4.1 16.3 16.2 19.1 9.1, 9.2, 9.3 3.2 14.1 18.1 15.2 18.2 23.1 19.5 4.16 17.1 17.2 21.1 15.1

27.2 Chunks

Chunk Name Defined in Used in
ChookDoor: class ChookDoor 11.3 11.1
ChookDoor: main 11.15 11.1
ChookDoor: misc routines 11.2 11.1
Event Manager: Clock 20.3 20.1
Event Manager: Event class 20.2 20.1
Event Manager: class eventManager 20.5 20.1
Event Manager: get next event 20.7 20.5
Event Manager: handle event 20.8 20.5
Event Manager: main 20.4 20.1
Event Manager: register event 20.9 20.5
Event Manager: sort events 20.6 20.5
Event Server: calling points 8.5 8.3
Event Server: log message handling 8.4 8.3
Event Server: main routine 8.7 8.3
Event Server: serverprocess routine 8.6 8.3
Event class: compare two events 6.2 6.1
Event class: definition 6.1 5.1
EventEditor: define get current events routine 9.5 9.1
EventEditor: define make edit page routine 9.7 9.1
EventEditor: define make home page routine 9.6 9.1
EventEditor: print instructions 9.4 9.1
EventList class: add event 7.2 7.1
EventList class: definition 7.1 5.1
EventList class: delete event 7.3 7.1
EventList class: load events 7.6 7.1
EventList class: nextEvent 7.5 7.1
EventList class: save events 7.7 7.1
EventList class: sort events 7.4 7.1
EventServerRPCaddress 8.2 2.3, 9.1, 10.1
EventServerRPCport 8.1 8.2, 8.3
HouseData define getTemps 19.3 19.2
HouseData define maxminTemp 19.4 19.2
HouseDefinitions: general routines 2.4 2.2
HouseDefinitions: server connections and interfaces 2.3 2.2
HouseMade: Relay Control 14.8 14.7
HouseMade: check client connection 14.13 14.3
HouseMade: collect date and time data 14.12 14.3, 14.16
HouseMade: define the Generate Solar Data routine 14.10 14.2
HouseMade: define the Generate Tank Data section 14.11 14.2
HouseMade: define the Generate Weather Data routine 14.9 14.2
HouseMade: define the house interface 14.3 14.2
HouseMade: generate the web page content 14.14 14.3
HouseMade: get events information 14.6 14.3
HouseMade: get local information 14.4 14.3
HouseMade: get relay information 14.5 14.3
HouseMade: legacy code for 14.7 14.3
Makefile: BeagleBone makes 25.3 25.1
Makefile: RelayServer makes 25.2 25.1
Makefile: install bastille 25.4
RelayServer: connect to the BeagleServer 4.2 4.1
Web: define the heatingData class 14.17 14.16
Web: heating: build widths for web page table 14.19 14.16
Web: heating: collect parameters and update 14.18 14.16
check client IP address OK 14.20
class ChookDoor: chookDoor 11.10 11.3
class ChookDoor: closeDoor 11.9 11.3
class ChookDoor: compute 11.6 11.3
class ChookDoor: doorState 11.11 11.3
class ChookDoor: handleEvent 11.12 11.3
class ChookDoor: init 11.4 11.3
class ChookDoor: load 11.5 11.3
class ChookDoor: openDoor 11.8 11.3
class ChookDoor: run 11.13 11.3
class ChookDoor: save 11.7 11.3
class ChookDoor: stop 11.14 11.3
current date 26.2
current version 26.1
house make temperature panel 14.15
relayserver: countDown 4.15 4.1
relayserver: define getTank 4.11 4.1
relayserver: define the RPC-Server interface 4.4 4.1
relayserver: getSolar 4.13 4.1
relayserver: getState 4.5 4.1
relayserver: getTimer 4.12 4.1
relayserver: readDoor 4.6 4.1
relayserver: setBit 4.8 4.1
relayserver: setBitOff 4.10 4.1
relayserver: setBitOn 4.9 4.1
relayserver: setState 4.7 4.1
relayserver: start 4.14 4.1
relayserver: strState 4.3 4.1

27.3 Identifiers

Identifier Defined in Used in
Clock 20.3 20.4
EventList 7.1
fastmode 20.4 20.4, 20.4
heating 14.16
house 14.3
hysteresis 16.1
jobtime 14.12
testing 20.4 20.4

118 accesses since 20 Jul 2020, HTML cache rendered at 20210125:1633