I wanted to bring ageing, and failing, Home Automation infrastructure into the modern age whilst retaining hardware investments already made.
Our house had Universal Power Bus (UPB) dimmers and switches installed at build time (2009). The controller is an HAI OMNI II which functions as both an alarm and automation platform. Over time this design has not kept up with modern trends and offers no upgrade paths (The overall company was taken over and simply discontinued all work on the existing products).
For example, the unit will phone the owner in the event of an alarm, fire etc. But those kinds of phone lines don’t exist anymore!
The relays controlling the watering system have failed one by one.
I can live without fancy scene lights but I do want something to do the watering for me – especially when we go away.
So, time to invest a bit of effort into getting the whole lot up to a modern standard.
Priority one was to remove the home automation functions from the old platform and rehost them. Leaving the OMNI with only the alarm function. The most important to change over was the watering system as, being silly, I was doing this in summer so it had to have a minimum change over time.
I envisaged a three-phase plan:
Phase 1: Redo existing stuff
Existing Equipment
- USB Serial adapter
- UPB PIM
Phase 2: Add Alexa for voice commands
Phase 3: Add Z-wave /ZigBee
Having just been caught with a proprietary device that can no longer be supported or upgraded I elected to follow the open-source route. With not much investigation I settled on Home Assistant. It will run on a Raspberry Pi and promised to integrate all the existing equipment and open the doors to new adventures. It will run in several modes but I choose to let it have a go and install its own operating system.
The next decision was to try and look professional this time! This is the rat’s nest bolt-on that was providing ‘services’ to replace the failed relays and missing phone lines.
It is based on an Arduino UNO with a relay shield then an internet shield, then more relays and on top a hand-crafted 12V to xV boost-buck and input voltage converter. I think we can agree it doesn’t look professional.
I started the POC using a spare Raspberry Pi 3B+ that I had. It quickly became apparent that the first spanner in the works was that Home Assistant had recently removed support for access to the GPIO pins and protocols such as I2C. There is a third-party component available that restored a fair bit of simple functionality - but not I2C.
I was hoping to use the very nice 8 Relays Card V2 for Raspberry Pi which uses I2C. It’s pricy but seeing how much hassle relays have caused in the past I wanted something decent.
In the end, I decided to split the functionality and install a second Raspberry Pi (running normal Raspberry Pi OS) to handle the physical aspects especially based on the relays.
- Raspberry Pi 4B - I chose the NUC bundle to get the Power Supply, case, and SD card as well
- Raspberry Pi 3B+
- 3 x 4 channel level converters 12V-3.3V
- 8 Relays Card
- DIN rail (from local electrical supplier)
- GPIO Ribbon Cable
- DIN Rail 2x20 IDC to Terminal Block Adapter
- DIN Rail Mount Bracket for Raspberry Pi
- USB to Serial Adapter (only needed to talk to UPB PIM)
- UPB PIM - only if using Universal Power Bus (UPB) - if you are using UPB you most probably already have one of these.
- Adafruit Perma-Proto Half-Sized Breadboard
With the goodies in hand, I started assembly on the bench and using the DIN rail. Firstly the Raspberry Pi 4 in the beautiful Argon ONE case. Then the IDC to terminal block Adapter. It was immediately apparent that the GPIO Ribbon Cable was going to be too short. I tried turning bits sideways etc but it was never going to reach - especially using the case. Then I discovered online that others were just using old IDE cables (everybody must have a box of these lying around).
I used a solderless breadboard with a few LEDs and push button switches to learn how to configure the HA OS to use the GPIO as input and output.
I disconnected the UPB PIM serial cable from the old controller and reattached it to the raspberry with a USB to serial adapter. Fired up the UPB Integration and imported the base configuration and bingo all the old lights/dimmers and switches are there and responding to commands. I couldn’t believe how easy that step was but I suppose it was using an existing configuration where a lot of work went in a decade ago.
I thought I should be able to just piggyback the PIR sensors ‘Alarm’ connections for use by the new hardware. In general, I have never had any problem connecting inputs together. In this case, I discovered that the OMNI was VERY sensitive to this. Its actual circuitry has not been published but it has a concept of ‘loop value’ that can be seen in its programming tool.
Using just the simple connection sent this to a value of ‘TROUBLE’ which caused the alarm console to beep and demand attention. In short, it did not like being tampered with.
Now a PIR is just a Normally Open switch across its two ‘Alarm’ connections (Green and Yellow in the diagram below) but in this case, I wanted the device to remain connected to the OMNI, to act as an alarm trigger, AND to be connected to the Home Automation device, to act as a general sensor input.
I did extensive measurements across a PIR’s terminals whilst it was connected to the OMNI only to conclude that it magically managed to float with no real reference to the Zone -ve nor to primary +12V. This was out of my league so I consulted my friend Roger Young, a retired electronic engineer. He came up with several circuits but again without knowing what the voltage was really with reference to they all suffered from similar ‘TROUBLE’. In the end, a simple transistor switch was closed. Referring to the diagram below I then proceeded to fiddle with the R1 and R2 resistor values until
- the ‘loop’ value fell into the ‘SECURE’ range and
- the collector output voltage was sufficient for the DFR0913 level converter to register it as a positive.
The values of 2.2K and 22K satisfied these and the result was a definite 0V or 3.3V digital level out of the level converter. The DFR0913 seems to not be a purely digital device - if it’s fed with half-baked input voltages then it can give half-baked output voltages which leaves it up to the RPi to determine if it's True or False.
Now all I had to do was get 8 of these circuits together - one for each PIR.
Finally - Had I forgotten that ‘Priority One’ was the watering so how to connect the Home Assistant - let’s call it ‘Command Centre’ with the slave Raspberry Pi 3 with the relays.
I decided to just use MQTT via the LAN connection. Let me explain my reasoning here. It’s not really using ‘the internet’ just those wires connected to a switch in the same cabinet so the activity remains local. In addition, if there is no power then there is nothing for the ‘Home Automation’ to do. It cannot turn on or off any lights. Nor open or close the garage door. There is no 24VAC to activate the watering solenoids and so on. So there is no particular requirement to handle a power outage. (The remaining Alarm part of the OMNI does have a battery and continues to function during a power outage.)
For those who are unfamiliar with MQTT, it uses a publish/subscribe model and is pretty easy to set up between two Raspberry Pis. (A thousand times easier than between a Pi and an Arduino which does not have an RJ45 connection!)
The message has a structure based around a ‘Topic’ and a ‘Payload’. And ‘Topics’ can have a hierarchy. Now, from bitter experience, if you are looking at a ‘request’ / ‘response’ model you have to use different topics so the ‘sender’ can subscribe to the response without also subscribing to its own message. The message plan I settled on is:
# HomeAssistant (RPi4) jc-home-ext (Rpi3) # /garage/door-open/ON ----> # /garage/door-close/ON ----> # /watering/ALL/OFF ----> # /watering//{ON|OFF|STATE} ----> # <---- /watering-state/ payload={ON|OFF} # /watering/state ----> # <---- /watering-state/ payload={ON|OFF} # /heartbeat ----> # <---- /heartbeat-response/
So the RPi4 needs to subscribe to ‘watering-state/#’ and ‘heartbeat-response’ whilst the slave Rpi3 subscribes to "watering/#", "garage/#", and "heartbeat/#". It’s a little bit more confusing in HomeAssistant than the above - but that can be another post.
Another thing I have learned the hard way with distributed processing like this is that each unit needs to be defensive. Any node cannot assume other nodes stay conscious (or rational for that matter).
In this case with the slave Rpi3, when we are turning on or off a watering system - where the duration/intelligence is not implemented here; then it is wise to have a safety stop in case the controlling module loses the plot and forgets to turn it off. So far this has only been triggered once for real and that was me rebooting the Rpi4 not realising it was in a watering cycle at the time.
The attached code also includes functionality to remind the user (me) that the watering is turned off. I added this here because it was easier to port it from the Arduino C to Python than to bother with configuring it in Home Assistant (so I can be lazy too). And it had been added to the Arduino version because, more than once, I ‘turned off’ the master water switch whilst we were in a wet spell and then forgot about it only to have the plants start to die - maybe a moisture sensor would be a better answer! That could be my next mini-project.
When it came to connecting it all up I put the hand-built isolator in the cabinet with the OMNI and connected the yellow wire from each of the PIR ‘zones’ to one input. Keeping track of which zone of course - otherwise we could have moved in, say, the dining area by turning on a light in the Garage! Fortunately when originally wired the electricians had labelled the cables coming into this cabinet.
The other end of each of these circuits is a common ‘Aux GND’.
I sent each of these circuits as discrete pairs to the level converter so this involved 8 pairs plus 1 more for ‘Alarm Triggered’ directly from the OMNI. So 18 wires and seeing a pile of 6 wire cables lying around I just used 3 strands from the OMNI cabinet to the ‘Automation’ cabinet.
All these needed to be isolated and converted to 3.3V for consumption by the Raspberry so those three strands went to the level converters.
Then using a few more lengths of the same 6-wire cable the outputs of the level converters went to the GPIO ports for the RPi. Well, the breakout connector block is at least connected to the RPi by a 40-pin cable. Each input was assigned a pretty arbitrary pin or port - just avoiding the pins used for power or ground.
A simple spreadsheet kept track of the route, wire colour etc for each input.
Wiring the slave Raspberry Pi was much simpler. I just pulled the cables to the water solenoids down to the new device and attached them to the appropriate relay as defined in the code
"Ferns-(Yellow)" : 1 "Veg1-(Red)" : 2 "General-(Green)" : 3 "General-(White)" : 4 "Veg2-(Green-Yellow)" : 5 "door-open" : 6 "door-close" : 7
Basically substituting Raspberry-controlled relays for Arduino-controlled ones but with the same input and output.
To activate the inputs to the RPI 4 Home Assistant required entries to the configuration.yaml (a sample)
binary_sensor: - platform: rpi_gpio sensors: - port: 17 name: "PIR Entry" unique_id: "pir_entry_sensor_port_17" bouncetime: 80 pull_mode: "DOWN" - port: 27 name: "PIR Dining" unique_id: "pir_dining_sensor_port_27" bouncetime: 80 pull_mode: "DOWN" - port: 10 name: "Alarm Triggered" unique_id: "alarm_triggered_sensor_port_10" bouncetime: 80 pull_mode: "DOWN"
And just to whet your appetite. Two of the automations that result. Firstly the sending of a SMS if the alarm is triggered. (The 5 second wait is because this input goes high for a second when the alarm is set - it ‘squarks’)
alias: Alarm Triggered description: send message about alarm trigger: - platform: state entity_id: - binary_sensor.alarm_triggered_2 from: "off" to: "on" for: hours: 0 minutes: 0 seconds: 5 condition: [] action: - service: notify.clicksend data: message: "Alarm Has been triggered! " title: House Alarm triggered - service: input_boolean.turn_on data: {} target: entity_id: input_boolean.alarm_has_been_triggered mode: single
Secondly turning on the entry light for a time, triggered by movement detection of the entry PIR.
alias: Entry Light - Turn On description: Activate entry light trigger: - platform: state entity_id: - binary_sensor.pir_entry from: "off" to: "on" condition: - condition: state entity_id: input_boolean.auto_sunset_sunrise_lights_active state: "on" action: - service: timer.start data: duration: "|| states('input_number.entry_light_duration_sec') | int }}" target: entity_id: timer.entry_light_timer - type: turn_on device_id: 64e661ff3fbed51da72c2d957957ceaa entity_id: light.entry_dimmer domain: light brightness_pct: 100 mode: single
I am still setting up HomeAssistant dashboards but here are some samples.
Now I am having fun putting Zigbee things all over the place. And my wife has taken to using Alexa for all sorts of things. So thanks to an infrastructure upgrade our Home automation has a lot more options and ‘conveniences’ now.
############################################################################## # watering_ctrl_1v0.py # The 'slave' for handling watering relays in Home Automation (Homeassistant) # communication occurs via mqtt # HomeAssistant (RPi4) jc-home-ext (Rpi3) # /garage/door-open/ON ----> # /garage/door-close/ON ----> # /watering/ALL/OFF ----> # /watering//{ON|OFF|STATE} ----> # <---- /watering-state/ payload={ON|OFF} # /watering/state ----> # <---- /watering-state/ payload={ON|OFF} # /heartbeat ----> # <---- /heartbeat-response/ # # unit relay on-for # Ferns-(Yellow) 1 0 0=leave on, controller has to turn off # Veg1-(Red) 2 0 # General-(Green) 3 0 # General-(White) 4 0 # Veg2-(Green-Yellow) 5 0 # door-open 6 0.5 >0 will time.sleep(x) then turn off # door-close 7 0.5 # # safety_timer behaviour: The purpose is to not let water run for ages # so 1. when you get an ON start the timer. If it was already running it should discard the old and start a new one # 2. when you get an OFF scan all active relays. If they are OFF then cancel() timer # 3. if the timer fires it means something has been turned ON but not OFF - most likely a communication disconnect so turn them all OFF # # no_water_timer behaviour: The purpose is to let humans know if nothing has been triggered for > 1 day - # ie 'remember you have disabled water' OR something is broken # so 1. when you get an ON start the timer. If it was already running it should discard the old and start a new one # 2. if the timer fires it.. send reminder sms then restart the timer for tomorrow; do this a daily for 7 days # # 1v23 * jc 2023/01/01 Report every state change # ############################################################################## import sys import time from datetime import datetime import requests import threading #import json import logging from logging import Formatter from logging.handlers import TimedRotatingFileHandler import paho.mqtt.client as mqtt import lib8relind ############################################################################## ####### Globals ############################################################################## G_MyName = "watering_ctrl_1v23.py" # G_MyNameShort = "water_1v23" G_MyVersion = "1v23" G_mainLoopCounter = 0 G_shutdown_triggered = False G_end_program_triggered = False G_stop_threads = False G_max_no_water_messages = 7 # normally 7 being 7 days G_no_water_message_num = 0 # these are sleep times G_timer_throttle_main_loop = 0.01 # global variables concerned with the message queue G_PublishAsDevice = "jc-home-ext-for-test" G_mqtt_connected_ok = False ## Instantiate an mqtt Client G_mqtt_client = mqtt.Client(G_PublishAsDevice) G_unit_relay_dict = { # unit (key) relay on_for domain "Ferns-(Yellow)" : [1, 0, 'water'], "Veg1-(Red)" : [2, 0, 'water'], "General-(Green)" : [3, 0, 'water'], "General-(White)" : [4, 0, 'water'], "Veg2-(Green-Yellow)" : [5, 0, 'water'], "door-open" : [6, 0.5, 'door'], "door-close" : [7, 0.5, 'door'] } # global variables concerned with the logging... G_logFolderBase = '/home/pi/logs/' G_log_format = ( ##'%(asctime)s:%(levelname)s: %(message)s') ##"%(asctime)s [%(levelname)s]: %(message)s in %(pathname)s:%(lineno)d") ##"%(asctime)s [%(levelname)s]: %(message)s in %(module)s:%(lineno)d") ##"%(asctime)s [%(levelname)s]:%(module)s:%(lineno)d :-> %(message)s") "%(asctime)s [%(levelname)s]:-> %(message)s") # format "%(asctime)s [%(levelname)s]:%(module)s:%(lineno)d :-> %(message)s" # produces.... # 2022-08-07 09:31:44,589 [INFO]:logger_child:10 :-> another message # 2022-08-07 09:31:44,589 [INFO]:logger_child:10 :-> final call to child1 # trying 1st with a single logger to rotating disk file and screen # when it goes live change level to INFO # Logging levels are # CRITICAL 50 # ERROR 40 # WARNING 30 # INFO 20 # DEBUG 10 # NOTSET 0 G_log_level = logging.DEBUG G_safety_timeout_sec = 1800.0 # 30mins*60 = 1800 G_no_water_timeout_sec = 90000.0 # 25hrs *60 *60 = 90000 # global variables concerned with the message queue # for real.. G_broker_url = "192.168.1.54" G_broker_name = "Homeassistant" # for test.. G_broker_port = 1883 G_broker_user = '########' G_broker_pwd = '########' G_send_sms_to_nums = '555....,555....' # G_safety_timeout_sec # global variables concerned with sending sms... G_sms_URL = 'http://api.smsbroadcast.com.au/api.php' G_sms_params = { 'username' : '####' , 'password' : '#####' , 'from' : 'Home' , 'to' : G_send_sms_to_nums , 'message' : 'x y z' } ############################################################################## ####### Threaded functions ############################################################################## # we cannot re-start a timer. it has to be cancelled and a new one created def newSafetyTimer(): global G_safety_timer G_safety_timer = threading.Timer(G_safety_timeout_sec, handle_safety_stop) def newNoWaterTimer(): global G_no_water_timer G_no_water_timer = threading.Timer(G_no_water_timeout_sec, handle_no_water_reminder) ############################################################################## ####### Call-back Functions for Threads ############################################################################## ############################################################################## # handle_safety_stop # This is the call back routine for the safety timer # this is called if the timer reaches zero # once it exits the thread is destroyed # it must : # * turn off all relays # * issue SMS saying - 'watering safety stop has occured' ############################################################################## def handle_safety_stop(): global G_sms_params lib8relind.set_all(0,0) # all off main_log.info("Watering safety stop has occured") G_sms_params['message'] = 'Watering safety stop has occured.' r = requests.get(G_sms_URL,params=G_sms_params) main_log.info(r.text) # main_log.info(r.url) ############################################################################## # handle_no_water_reminder # This is the call back routine for the G_no_water_timer # this is called if the timer reaches zero # once it exits the thread is destroyed # it must : # * send reminder sms then # * restart the timer for tomorrow; # do this a daily for 7 (G_max_no_water_messages) days ############################################################################## def handle_no_water_reminder(): global G_sms_params global G_no_water_message_num main_log.debug("no-water timer has triggered") if G_no_water_message_num < G_max_no_water_messages : G_no_water_message_num += 1 l_message = 'Watering has not triggered for over a day. Msg Num = ' + str(G_no_water_message_num) if G_no_water_message_num == G_max_no_water_messages: # after incrementing l_message = l_message + ' (last reminder)' G_sms_params['message'] = l_message main_log.info('Message=' + G_sms_params['message'] ) r = requests.get(G_sms_URL,params=G_sms_params) main_log.info(r.text) # main_log.info(r.url) newNoWaterTimer() G_no_water_timer.start() ############################################################################## #### Non- threaded general functions ############################################################################## #################################### # set_all_relays_off occurs often enough to put it in its own function # it will iterate the list so that it can also send state messages def set_all_relays_off(): for x in G_unit_relay_dict: set_relay(x, "OFF") # if you want belt and braces ## lib8relind.set_all(0,0) # all off #################################### # every state change to a water relay needs to send a 'watering-state/' message as well # takes # p_relay_dict_key an index/key into G_unit_relay_dict so it can look up the relay number # p_new_state ON|OFF def set_relay(p_relay_dict_key, p_new_state): new_relay_state = 0 if p_new_state == 'OFF': new_relay_state = 0 elif p_new_state == 'ON': new_relay_state = 1 else: main_log.info("invalid new_state passed to set_relay = " + p_new_state ) return relay_num = G_unit_relay_dict[p_relay_dict_key][0] lib8relind.set(0,relay_num,new_relay_state ) # lib8relind.set(0,{relay_num},{0=off | 1=ON} ) if G_unit_relay_dict[p_relay_dict_key][2] == 'water': new_msg_state = (p_new_state.lower()).capitalize() # will use initcap for showing on dashboard publish_one_message('watering-state/' + p_relay_dict_key , message=new_msg_state) ############################################################################################ ############################################################################## # The mqtt client is now a global as G_mqtt_client # this needs more work - some how it has to handle reconnects and also wait until broker is alive (especially seeing its on another computer) def mqtt_initialisation(): mod = '(mqtt_initialisation) ' global G_mqtt_connected_ok ## establish callback routines - actions to take when events occur G_mqtt_client.on_connect = on_mqtt_connect G_mqtt_client.on_disconnect = on_mqtt_disconnect G_mqtt_client.on_message = message_received G_mqtt_client.username_pw_set(G_broker_user, G_broker_pwd) try: G_mqtt_client.connect(G_broker_url, G_broker_port) except: main_log.info(mod + "Error on connect attempt.. retrying") time.sleep(10) return G_mqtt_connected_ok = True G_mqtt_client.loop_start() main_log.debug(mod + "after Mqtt client loop start") def on_mqtt_connect(client, userdata, flags, rc): main_log.info("MQTT Connected With Result Code " + str(rc)) ## happens at start and if re-connected if rc == 0: do_subscribe() def on_mqtt_disconnect(client, userdata, rc): main_log.info("MQTT Client Got Disconnected With Result Code " + str(rc)) ## this appears if, say, mosquitto goes offline def do_subscribe(): mod = '(do_subscribe) ' ## subscribe to messages we want to handle # HomeAssistant jc-home-ext # /watering//{ON|OFF|STATE} ----> # <---- /watering-state/ payload={ON|OFF} # /watering/state ----> # <---- /watering-state/ payload={ON|OFF} # /watering-control/end-program/x ----> (3rd parameter is dummy) # /heartbeat ----> # <---- /heartbeat-response/ G_mqtt_client.subscribe("watering/#" , 0) G_mqtt_client.subscribe("garage/#" , 0) G_mqtt_client.subscribe("watering-control/#" , 0) G_mqtt_client.subscribe("heartbeat/#" , 0) main_log.debug(mod + "after client.subscribe") ############################################################################## ####### message_received ####### handle the incoming messages # only the ones we subscribed to will be seen here so thats all we will ever need to deal with # message.topic holds the full string eg /watering//{ON|OFF|STATE} # local_command holds it without the lead tokens (ie just "//{ON|OFF|STATE}) def message_received(client, userdata, message): mod = '(message_received) ' global G_end_program_triggered global G_no_water_message_num if 'heartbeat' not in message.topic.strip(): main_log.info(mod + "*. Message Recieved re topic " + message.topic + " message reads " + message.payload.decode()) if message.topic.strip() == 'heartbeat': main_log.debug(mod + "*. Heartbeat message received") publish_one_message('heartbeat-response', message=G_PublishAsDevice) return token_list = message.topic.split("/",2) local_message_subject = token_list[0].strip() try: local_message_unit = token_list[1].strip() except: local_message_unit = '' try: local_command = token_list[2].strip() except: local_command = '' # main_log.debug (" message.topic =" + message.topic) # eg 'watering/Veg1-(Red)/ON' # main_log.debug (" local_message_subject =" + local_message_subject) # eg 'watering' # main_log.debug (" local_message_unit =" + local_message_unit) # eg 'Veg1-(Red)' # main_log.debug (" local_command =" + local_command) # eg 'ON' if local_message_subject == 'watering-control': if local_message_unit == 'end-program': G_end_program_triggered = True main_log.info("Program end message received.. triggered") return # Handle non-relay specific commands if local_message_subject == 'watering': if local_message_unit == 'ALL': if local_command == 'OFF': for x in G_unit_relay_dict: if G_unit_relay_dict[x][2] == 'water': set_relay(x, "OFF") main_log.info(" watering ALL/OFF actioned") try: G_safety_timer.cancel() main_log.debug(" safety timer canceled") except: # ignore - the cancel will fail if the timer does not exist. ie if OFF is repetitively called xy =1 newSafetyTimer() # trying to keep one of these 'declared' at all times else: main_log.info("invalid command recieved for 'watering/ALL' = " + local_command ) return # end of 'watering/ALL' # 'state' # this is one message in that results in many out - one for each watering relay if local_message_subject == 'watering': if local_message_unit == 'state': # iterate the relay list and ask each in turn for its status. # for each one send a message # /watering-state/ message/pyload = On|Off for x in G_unit_relay_dict: # x is the key name (a 'str' eg "Ferns-(Yellow)") if G_unit_relay_dict[x][2] == 'water': one_relay = G_unit_relay_dict[x][0] l_state = lib8relind.get(0,one_relay ) # returns 0 for OFF and 1 for ON if l_state == 0: l_state_txt = 'Off' else: l_state_txt = 'On' publish_one_message('watering-state/' + x , message=l_state_txt) main_log.info(" watering state actioned") return # end of 'watering/state' ############################ # Validate Unit try: l_relay = G_unit_relay_dict[local_message_unit][0] except: main_log.info("invalid 'unit' received = " + local_message_unit ) return ############################ # Validate and action commands l_on_for_time = G_unit_relay_dict[local_message_unit][1] if local_command == 'ON': set_relay(local_message_unit, 'ON') if l_on_for_time != 0: # if an 'on time' has been specified then do it all now main_log.info(" actioning unit = " + local_message_unit + ' relay: ' + str(l_relay)) time.sleep(l_on_for_time) set_relay(local_message_unit, 'OFF') return G_safety_timer.cancel() newSafetyTimer() G_safety_timer.start() G_no_water_timer.cancel() newNoWaterTimer() G_no_water_timer.start() G_no_water_message_num = 0 main_log.info(" watering started on unit = " + local_message_unit + ' relay: ' + str(l_relay)) return if local_command == 'OFF': set_relay(local_message_unit, 'OFF') main_log.info(" watering finished on unit = " + local_message_unit + ' relay: ' + str(l_relay)) l_on_count = 0 for x in G_unit_relay_dict: one_relay = G_unit_relay_dict[x][0] if G_unit_relay_dict[x][2] == 'water': l_on_count = l_on_count + lib8relind.get(0,one_relay ) # returns 0 for OFF and 1 for ON if l_on_count == 0 : main_log.debug(" safety timer about to be stopped - all relays are OFF") try: G_safety_timer.cancel() main_log.debug(" safety timer canceled") except: # ignore - the cancel will fail if the timer does not exist. ie if OFF is repetitively called xy =1 newSafetyTimer() # trying to keep one of these 'declared' at all times return if local_command == 'STATE': l_state = lib8relind.get(0,l_relay ) # returns 0 for OFF and 1 for ON if l_state == 0: l_state_txt = 'Off' else: l_state_txt = 'On' publish_one_message('watering-state/' + local_message_unit , message= l_state_txt) return main_log.info("*. Message Recieved re " + local_message_unit + " but not handled re topic " ) ## <<< end of message_received ############################################################################## ####### publish_one_message ####### this version does not have 'from' / 'to' ############################################################################## def publish_one_message(topic, message=None): mod = '(publish_one_message) ' if message is None: main_log.debug(mod + " publishing topic=" + topic + " message= (none)" ) else: main_log.debug(mod + " publishing topic=" + topic + " message=" + message ) G_mqtt_client.publish(topic, message) ############################################################################## ############################################################################## ####### Main functions ############################################################################## ############################################################################## ##### initialise() mod = '(main) ' ############################################################################## #### Get log file to rotate # valid values for 'when' # second (s) # minute (m) # hour (h) # day (d) # w0-w6 (weekday, 0=Monday) # midnight # and interval is how many # so when="d", interval=7, is every 7 days ############################################################################## #create_timed_rotating_log() ##MAIN_LOG_FILE = G_logFolderBase + "main_log_" + str(time.strftime("%Y%m%d-%H%M%S")) + ".log" MAIN_LOG_FILE = G_logFolderBase + "main_log_" + ".log" main_log = logging.getLogger("watering.main") main_log.setLevel(G_log_level) main_log_stream_handler = logging.StreamHandler() main_log_stream_handler.setLevel(G_log_level) main_log_stream_handler.setFormatter(Formatter(G_log_format)) main_log.addHandler(main_log_stream_handler) main_log_file_handler = TimedRotatingFileHandler(MAIN_LOG_FILE, when="d", interval=1, backupCount=10) main_log_file_handler.setLevel(G_log_level) main_log_file_handler.setFormatter(Formatter(G_log_format)) main_log.addHandler(main_log_file_handler) main_log.info("I am " + G_MyName + " starting") main_log.info("Connecting mosquitto to " + G_broker_name + " (Url= "+ G_broker_url + ") for commands" ) ## make sure all relays start OFF, and report status of watering relays as well set_all_relays_off() # instantiate timers in global space newSafetyTimer() newNoWaterTimer() G_no_water_timer.start() # and actually start the no-water one ## establish callback routines - actions to take when events occur ## and start the mqtt loop while G_mqtt_connected_ok == False: mqtt_initialisation() # log the start time current_time = time.strftime("%H:%M:%S", time.localtime()) main_log.info(mod + "Starting Main Loop at time=" + current_time) starttime = time.time() ############################################################################## ##### Real working main while True: G_mainLoopCounter = G_mainLoopCounter + 1 if G_shutdown_triggered == True : main_log.info("Shutdown request detected") break if G_end_program_triggered == True : main_log.info("Program end requested") break ### ****************************************** ### WARNING ### basically at idle with no 'sleep' ### mainloop can do over 473,000 cycles/sec ### ****************************************** # something not dependant on timers if (G_mainLoopCounter % 10000 == 0) : ## just hint where we are up to current_time = time.strftime("%H:%M:%S", time.localtime()) endtime = time.time() main_log.info(mod + 'Main Loop count=' + str(G_mainLoopCounter) \ + ' at increment time=' + current_time + ' thats ' + str(int(G_mainLoopCounter / (endtime - starttime))) + ' loops/sec') # yield some time to other threads time.sleep(G_timer_throttle_main_loop) ## end loop main_log.info(mod + "{0} end of main 'while true' Loop" .format(G_mainLoopCounter)) #, end='\r',flush=True) ############################################################################## ## Cleanup / Finalise - especially threads and exit ## G_stop_threads is a variable that is checked inside each threaded module ## it is a trigger for them to 'exit' ## p.s. currently (initial state) there is no way to reach this code set_all_relays_off() # close down mqtt G_mqtt_client.loop_stop() time.sleep(0.2) G_stop_threads = True # join up (ie kill) any leftover real threads main_thread = threading.current_thread() for t in threading.enumerate(): if t is main_thread: continue main_log.debug('joining %s', t.getName()) t.join() main_log.info('threads killed') main_log.info("ending ..........")