AlarmPi: Using a Raspberry Pi Zero W to control a setback thermostat when an alarm system is armed/disarmed
December 30, 2019
This is the part of the home automation project that got all this started. We arm our alarm system each time we leave and disarm it when we return. Although the thermostat is located physically next to the alarm panel, one of us (not naming names here :-)), sometimes forgets to set the thermostat back upon leaving or return it to the running program upon return. My thought was: “If we have a WiFi controllable thermostat, why can’t the alarm system do that work automatically for us?”
Clearly, the alarm system has no way, natively, to do this. It has no connection to WiFi. I would need another device that could determine if the alarm system was armed/disarmed and perform the appropriate thermostat adjustments based on that determination.
I’ve had good luck with Raspberry Pi Zero W’s in the past (see: http://homeautomation.geiserweb.com/index.php/2018/08/16/a-weather-station-version-1/ for more information about my first Raspberry Pi project to interact with my WiFi thermostat). So it was natural that I would choose the same solution for this project. But how to get the state (armed/disarmed) of the alarm?
I spent some time looking around the Internet and spoke to one or two members at the makerspace of which I am a member. My alarm panel is reasonably old and I got no real ideas. I avoided calling the alarm system provider as I was trying to save the cost of a visit. Ultimately, I had to call him as no other solution presented itself.
He told me that he could hook a relay up that would close a contact when the alarm was armed and open the contact when it was disarmed. The only catch was that the relay would pulse should the alarm be tripped.
I wrote some code to attempt to “debounce” the switch messages the AlarmPi got from the alarm in case of someone tripping the alarm. It was not very elegant and was somewhat difficult to debug, but appeared to work.
I installed the AlarmPi next to the alarm panel and hooked the power to the backup battery that the alarm maintains. I didn’t think that the small Raspberry Pi Zero W would put much of a strain on the battery/charging circuitry. It worked well for many months.
Early one fall morning (i.e., around 2 am), we lost power for a short while due to a tree falling nearby. The alarm chirped to let me know it had lost power. After the power returned, the alarm contacted the central station to report that the battery was getting low. Although it was a relatively new battery, I bought and installed a replacement. Once again, it contacted the central station to tell them that the new battery was also getting low.
So, onto …
I removed all of my hardware and wiring from the alarm cabinet and moved it to a dedicated box in the basement. I ran a wire from the relay to the box and used the old battery connected to a trickle charger to power the AlarmPi.
I tested the functionality (that had been working fine). It worked … once! After the first time I tried it, it never worked properly again. I still am not sure why. As I scratched my head to try to figure it out, I thought of a much better way to handle the problem of the pulsing relay and so, I implemented …
The relay board my alarm system installer supplied actually had three relay contacts … a common, a normally-open contact, and a normally-closed contact. When the alarm was armed, the normally-open contact would close and the normally-closed contact would open. When the alarm was disarmed, they would return to their default state. By using the GPIOZero python library, I could trigger a function to be executed when each contact was “held” for a number of seconds thereby avoiding the pulsing relay problem.
I implemented this solution and it worked beautifully … once!
The code to set the thermostat back called a function I wrote to parse the day’s thermostat program to find the lowest temperature in the program in order to set the thermostat back to that value (I didn’t want any temperature values hard coded in the AlarmPi). I found that sometimes the thermostat would fail on one of the calls in that code.
My solution was to put that function call in a loop and wait a few seconds between tries. If it is unsuccessful after a few tries, AlarmPi will text me a message so I can set the thermostat back using the app on my smartphone. This seems to have gotten around the only glitch I’ve found, so far, with Solution #3.
This is the relay board added to my alarm panel. The two left-most wires are the signal coming from the alarm control board. The three wires on the right go to the basement AlarmPi box. The red wire (connected to the normally closed side of the relay) goes to the disarm code that sets the thermostat back. The white wire is the common pin. The green wire is (connected to the normally open side of the relay) goes to the arm code that returns the thermostat to running the current program.
This is the inside of the AlarmPi box in the basement. It is more complicated than it must be for just this project as I have a few other plans for items that will be installed in this box as well. Stay tuned.
The brown wire in the upper right is the one connected to the relay in the alarm box. It is connected to the black/yellow/red wires and connector that allow me to disconnect and remove the AlarmPi from the cabinet if necessary.
The purple box is the Raspberry Pi Zero W with a prototype board hat attached to which the relay control wires attach.
The box attached at the upper right (the one with the glare in the photo) is a dual USB connection that alters the voltage from the battery and provides the USB plug to power AlarmPi.
Out of the picture is the trickle charger. You can see the wire for it coming in on the left side just above the battery.
The circuit on the back of the door (left hand edge) is a voltage meter so I can keep tabs on the battery without opening the box.
The Raspberry Pi Zero W circuit is quite simple. The common connector from the relay attaches to the ground (pin 6), the normally closed relay connection attaches to GPIO17 (pin 11), and the normally open relay attaches to GPIO15 (pin 10) on the Raspberry Pi Zero W.
Python Code (partial)
""" alarm_tstat: Python app to alter RadioThermostat settings on alarm arm/disarm. """ ################################################################################ # # alarm_tstat.py - Control Radio Thermostat when a switch (i.e., alarm relay) is # armed/disarmed # # Copyright (C) 2019, Wayne Geiser. All Rights Reserved. # email: email@example.com # # You have no rights to any of this code without expressed permission. # ################################################################################ from time import sleep from gpiozero import Button from wg_helper import wg_trace_print from wg_helper import wg_error_print from wg_radio_thermostat import HOLD_DISABLED from wg_radio_thermostat import HOLD_ENABLED from wg_radio_thermostat import NIGHTLIGHT_OFF from wg_radio_thermostat import NIGHTLIGHT_ON from wg_radio_thermostat import RADTHERM_FLOAT_ERROR from wg_radio_thermostat import RADTHERM_INT_ERROR from wg_radio_thermostat import radtherm_get_todays_lowest_setting from wg_radio_thermostat import radtherm_set_float from wg_radio_thermostat import radtherm_set_int from wg_radio_thermostat import SAVE_ENERGY_MODE_DISABLE from wg_radio_thermostat import SAVE_ENERGY_MODE_ENABLE from wg_twilio import sendtext __version__ = "v3.1" TRACE = False TEST_MODE = False NO_RELAY_PIN_BCM = 15 NC_RELAY_PIN_BCM = 17 DEBOUNCE_SECONDS = 5.0 NUM_RETRIES = 5 # Number of times to retry a failed call def setback_tstat(button): """ Set the thermostat to the lowest temp setting on today's prog. """ wg_trace_print("Normally open switch held for " + str(button.active_time) + " seconds", TRACE) if TEST_MODE: return i = 0 setback_temp = RADTHERM_FLOAT_ERROR while i < NUM_RETRIES and setback_temp == RADTHERM_FLOAT_ERROR: setback_temp = radtherm_get_todays_lowest_setting(TRACE) if setback_temp != RADTHERM_FLOAT_ERROR: wg_trace_print("Setting target temp to " + str(setback_temp), TRACE) # set the temporary temperature to the value we found, above floatret = radtherm_set_float("t_heat", setback_temp, TRACE) if floatret == RADTHERM_FLOAT_ERROR: wg_error_print("setback_tstat", "Error setting t_heat") return # set the t-stat to hold intret = radtherm_set_int("hold", HOLD_ENABLED, TRACE) if intret == RADTHERM_INT_ERROR: wg_error_print("setback_tstat", "Error setting hold") return # turn the night light off intret = radtherm_set_int("intensity", NIGHTLIGHT_OFF, TRACE) if intret == RADTHERM_INT_ERROR: wg_error_print("setback_tstat", "Error setting intensity") return wg_trace_print("System armed", True) else: sleep(DEBOUNCE_SECONDS) i = i + 1 if setback_temp == RADTHERM_FLOAT_ERROR: # Send me a text message to tell me it didn't work sendtext("Unable to set thermostat back. You'll have to do it via smartphone app. Sorry.") def run_tstat(button): """ Run the current thermostat prog. """ wg_trace_print("Normally closed switch held for " + str(button.active_time) + " seconds", TRACE) if TEST_MODE: return # wait to see if we get anymore button presses in the next DEBOUNCE_SECONDS seconds # disable hold intret = radtherm_set_int("hold", HOLD_DISABLED, TRACE) if intret == RADTHERM_INT_ERROR: wg_error_print("run_tstat", "Error setting hold") return # set the tstat to SAVE_ENERGY_MODE intret = radtherm_set_int("mode", SAVE_ENERGY_MODE_ENABLE, TRACE) if intret == RADTHERM_INT_ERROR: wg_error_print("run_tstat", "Error enabling save energy mode") return # turn off SAVE_ENERGY_MODE intret = radtherm_set_int("mode", SAVE_ENERGY_MODE_DISABLE, TRACE) if intret == RADTHERM_INT_ERROR: wg_error_print("run_tstat", "Error disabling save energy mode") return # !!! Now should be running current program !!! # turn the night light on intret = radtherm_set_int("intensity", NIGHTLIGHT_ON, TRACE) if intret == RADTHERM_INT_ERROR: wg_error_print("run_tstat", "Error setting intensity") return wg_trace_print("System disarmed", True) def main(): """ alarm_tstat main code. """ wg_trace_print("Alarm/Tstat controller started. Version: " + __version__, True) armed_switch = Button(NO_RELAY_PIN_BCM, hold_time=DEBOUNCE_SECONDS) disarmed_switch = Button(NC_RELAY_PIN_BCM, hold_time=DEBOUNCE_SECONDS) armed_switch.when_held = setback_tstat disarmed_switch.when_held = run_tstat # Make sure we start out disarmed and the tstat is running it's program running = True run_tstat(disarmed_switch) while running: sleep(1) main()
Note that I am reasonably new to python programming. It is likely that there is a better way to write this code. If you have constructive suggestions, please send them in an email.
The main() function simply sets up the GPIOZero buttons for the two pins we’ve wired to the alarm panel relay, sets up the length of time to detect a “hold” condition and which functions to call when that condition is met.
setback_tstat is called when the armed_switch is held. It determines the lowest temperature setting in today’s thermostat program, sets the thermostat to that setting, puts the thermostat in the “hold” state so that no adjustments will be made while the alarm is armed, and turns the screen backlight off to indicate that it worked.
run_tstat is called when the disarmed_switch is held. It turns off the “hold” state, turns “save energy” on and off to resume running the current program, and turns the screen backlight on again.
I haven’t included the wg_helper routines, twilio code (implements the Text messaging functionality), nor the radio_thermostat code here. If you are interested in the complete implementation, send me an email. Some of these modules will need personal data cleaned from them before they may be shared.