Smart Home Makeover with HomeAssistant

Updated 15 May 2023

Introduction

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.

Choosing The Architecture

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.

Assembly

Assembly

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

  1.  the ‘loop’ value fell into the ‘SECURE’ range and
  2.  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.

Wiring The Components Together

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.

Full Code

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

Have a question? Ask the Author of this guide today!

Please enter minimum 20 characters

Your comment will be posted (automatically) on our Support Forum which is publicly accessible. Don't enter private information, such as your phone number.

Expect a quick reply during business hours, many of us check-in over the weekend as well.

Feedback

Please continue if you would like to leave feedback for any of these topics:

  • Website features/issues
  • Content errors/improvements
  • Missing products/categories
  • Product assignments to categories
  • Search results relevance

For all other inquiries (orders status, stock levels, etc), please contact our support team for quick assistance.

Note: click continue and a draft email will be opened to edit. If you don't have an email client on your device, then send a message via the chat icon on the bottom left of our website.

Makers love reviews as much as you do, please follow this link to review the products you have purchased.