Several years ago I started a commercial production brewery with some friends. Our initial installation of our glycol chilling system for our fermenters was a very basic approach - 120V relays wired to basic temperature controllers that had temperature probes installed in the probe port of our three fermenters. Over time, we found this configuration to be extremely frustrating due to several factors:
- The relays were unaware of the glycol temperature - if it happened to be above the target temperature of a fermenter (e.g. we were crashing another fermenter), the relays would stay open and even begin to warm an already cooled/crashed brew.
- There was no way to monitor the temperature remotely, meaning that post-transfer from the kettle, we’d have to sit around and wait for the fermenter to hit target temp, checking it regularly by hand.
- Setting the target temperatures was manual, meaning it introduced human error. It was easy to accidentally set the target temperature inconsistently from batch to batch.
- There was no way to receive alerts if a malfunction occurred. It was completely possible to take a weekend off and come in to the brewery to find your beers weren’t at the temperature they need to be to finish properly.
I set out to resolve these problems, and settled on OpenHAB (OH) running on a Raspberry Pi. At the time of this selection, OpenHAB1 was the package of choice - since this time OH has moved to a version 2 release. The concepts in this document are applicable to OH2 but all samples are based on OH1. Since OH2 has a compatibility layer, I believe you could very simply implement this in OH2 with minimal changes, but I have not tried.
I had already had some experience using OH to automate my home, so this seemed like a low barrier to entry and I was very familiar with how robust the rulesets could be.
Required Hardware
The following hardware was utilized for this build:
- Raspberry Pi 3 (RPi3) with 16GB micro-SD storage
- A quality power supply for the Raspberry Pi (this matters, don’t skip it!)
- SainSmart 8-Channel Relay Module (I believe any model of this relay works, regardless of channels)
- HDMI 5" 800x480 Display Backpack - With Touchscreen
- Waterproof DS18B20 Digital temperature sensor + extras
- 3/4" 110V Brass Electric Solenoid Valve AC Normally Closed
- Spare 110V electrical cable
- Spare CAT6 cable
Required Software
• Raspbian running on the RPi
• OpenHAB installed
Setup and Configuration
Connecting the Relay
There are many examples of how to get the SainSmart relays working on the RPi. This video and info in the video description should be enough to get the basics connected. You will want to be specifically careful to identify which pin controls which relay - you will need to update a rules file later on.
Important Design Consideration
At this point, you have some design considerations to consider. You’ll notice we used Normally Closed (NC) solenoids, meaning that when no power is applied to the solenoid, it closes. When discussing with our brewer, he indicated he preferred that if any issue occurred with the RPi, that we would default to crashing the beers, versus letting them overheat. This is a critical decision for your design, but the remainder of this guide assumes that when wiring the solenoids to the RPi, that you connect solenoids to the relay in a manner such that when the relay is NOT activated, power passes through it to the solenoid. The core script that handles this is switch_solenoid.py, below. Note that when an “ON” command is received we actually send GPIO.HIGH to the relay, which technically tells the relay to switch to its default state (open across the two points furthest from each other on the relay), and an OFF command tells it to actually energize the relay and break the “normal” circuit. The red circled circuits below show how we connected the solenoids. When the relay is not receiving power, the relay defaults to completing the circuit through the solenoid and cooling the attached fermenters.
Connecting the Temperature Probes
Setting up the probes is pretty straightforward. In case you are unsure, you can connect multiple probes to a single resistor and run. I used CAT6 cabling to extend the length of the wiring from the temperature probes to the RPi, which was located several feet away. You can find instructions on Adafruit for connecting the probes.
Setting Up OpenHAB
For the purposes of this installation, you’ll need to make the decision as to how you’d like to install OpenHAB. I selected a snapshot zip build that I extracted to a folder at /srv/openhab, however, in hindsight I think I would have preferred to use a PPA to get the package via apt-get for simpler maintainability.
Items File
Below is a sample configuration of the items file we utilized at the brewery. You’ll notice three fermenters, three temperature monitors, and a few controls.
Group Temp_Fermenters
Group Target_Temps
Group Solenoid_State
Number Chart_Period ""
Number Temp_Fermenter_Ferdinand "Ferdinand [%.2f F]" <fermenter> (Temp_Fermenters) { exec="<[/srv/openhab/scripts/get_probe_temp.sh@@28-00000589d10c:10000:REGEX((.*?))]" }
Number Temp_Fermenter_Francis "Francis [%.2f F]" <fermenter> (Temp_Fermenters) { exec="<[/srv/openhab/scripts/get_probe_temp.sh@@28-00000588e71c:10000:REGEX((.*?))]" }
Number Temp_Fermenter_Frankie "Frankie [%.2f F]" <fermenter> (Temp_Fermenters) { exec="<[/srv/openhab/scripts/get_probe_temp.sh@@28-000005892d46:10000:REGEX((.*?))]" }
Number Temp_Glycol "Glycol [%.2f F]" <water> (Temp_Fermenters) { exec="<[/srv/openhab/scripts/get_probe_temp.sh@@28-000005887687:10000:REGEX((.*?))]" }
Number Target_Temp_Ferdinand "Ferdinand Set Temp [%.2f F]" <temperature> (Target_Temps)
Number Target_Temp_Francis "Francis Set Temp [%.2f F]" <temperature> (Target_Temps)
Number Target_Temp_Frankie "Frankie Set Temp [%.2f F]" <temperature> (Target_Temps)
Switch Solenoid_Ferdinand "Cooling Ferdinand [%s]" <switch> (Solenoid_State)
Switch Solenoid_Francis "Cooling Francis [%s]" <switch> (Solenoid_State)
Switch Solenoid_Frankie "Cooling Frankie [%s]" <switch> (Solenoid_State)
Rules File
And our rules file looks like this. You will want to find the lines matching:
executeCommandLine("/srv/openhab/scripts/switch_solenoid.py@@3@@ON"
And update the @@3@@ to indicate the appropriate pin number you identified controls each relay when you setup the RPi to control the relay.
Tempcontrol.rules
import org.openhab.core.library.types.*
import org.openhab.core.persistence.*
import org.openhab.model.script.actions.*
import java.lang.*
rule "Check Fermenter Temperatures"
when
Time cron "*/30 * * * * ?"
then
var Number deadrange = 1
var Number glycolrange = 1.1
if(Temp_Fermenter_Ferdinand.state > (Float::parseFloat(Target_Temp_Ferdinand.state.toString()) + deadrange) && Float::parseFloat(Temp_Glycol.state.toString())*glycolrange < Float::parseFloat(Temp_Fermenter_Ferdinand.state.toString()))
{
logInfo("Base Rules", "Ferdinand (" + Temp_Fermenter_Ferdinand.state + "F) warmer than target temperature (" + Target_Temp_Ferdinand.state.toString() + "F) plus dead range. Turning on relay.")
sendCommand(Solenoid_Ferdinand, ON)
}
else if(Temp_Fermenter_Ferdinand.state < Float::parseFloat(Target_Temp_Ferdinand.state.toString()))
{
logInfo("Base Rules", "Ferdinand temperature (" + Temp_Fermenter_Ferdinand.state + "F) equal or lower than target temp (" + Target_Temp_Ferdinand.state.toString() + "F). Turning off relay.")
sendCommand(Solenoid_Ferdinand, OFF)
}
if(Temp_Fermenter_Francis.state > (Float::parseFloat(Target_Temp_Francis.state.toString()) + deadrange) && Float::parseFloat(Temp_Glycol.state.toString())*glycolrange < Float::parseFloat(Temp_Fermenter_Francis.state.toString()))
{
logInfo("Base Rules", "Francis (" + Temp_Fermenter_Francis.state + "F) warmer than target temperature (" + Target_Temp_Francis.state.toString() + "F) plus dead range. Turning on relay.")
sendCommand(Solenoid_Francis, ON)
}
else if(Temp_Fermenter_Francis.state < Float::parseFloat(Target_Temp_Francis.state.toString()))
{
logInfo("Base Rules", "Francis temperature (" + Temp_Fermenter_Francis.state + "F) equal or lower than target temp (" + Target_Temp_Francis.state.toString() + "F). Turning off relay.")
sendCommand(Solenoid_Francis, OFF)
}
if(Temp_Fermenter_Frankie.state > (Float::parseFloat(Target_Temp_Frankie.state.toString()) + deadrange) && Float::parseFloat(Temp_Glycol.state.toString())*glycolrange < Float::parseFloat(Temp_Fermenter_Frankie.state.toString()))
{
logInfo("Base Rules", "Frankie (" + Temp_Fermenter_Frankie.state + "F) warmer than target temperature (" + Target_Temp_Frankie.state.toString() + "F) plus dead range. Turning on relay.")
sendCommand(Solenoid_Frankie, ON)
}
else if(Temp_Fermenter_Frankie.state < Float::parseFloat(Target_Temp_Frankie.state.toString()))
{
logInfo("Base Rules", "Frankie temperature (" + Temp_Fermenter_Frankie.state + "F) equal or lower than target temp (" + Target_Temp_Frankie.state.toString() + "F). Turning off relay.")
sendCommand(Solenoid_Frankie, OFF)
}
end
rule "Ferdinand Temperature Control"
when
Item Solenoid_Ferdinand received command
then
var String return_code = "0"
if(Solenoid_Ferdinand.state == ON)
{
return_code = executeCommandLine("python@@/srv/openhab/scripts/switch_solenoid.py@@2@@ON", 5000)
}
else
{
return_code = executeCommandLine("python@@/srv/openhab/scripts/switch_solenoid.py@@2@@OFF", 5000)
}
logInfo("Base Rules", "Ferdinand Solenoid Command Return Code: " + return_code)
end
rule "Francis Temperature Control"
when
Item Solenoid_Francis received command
then
var return_code = 0
if(Solenoid_Francis.state == ON)
{
return_code = executeCommandLine("/srv/openhab/scripts/switch_solenoid.py@@3@@ON", 5000)
}
else
{
return_code = executeCommandLine("/srv/openhab/scripts/switch_solenoid.py@@3@@OFF", 5000)
}
logInfo("Base Rules", "Francis Solenoid Command Return Code: " + return_code)
end
rule "Frankie Temperature Control"
when
Item Solenoid_Frankie received command
then
var return_code = 0
if(Solenoid_Frankie.state == ON)
{
return_code = executeCommandLine("/srv/openhab/scripts/switch_solenoid.py@@11@@ON", 5000)
}
else
{
return_code = executeCommandLine("/srv/openhab/scripts/switch_solenoid.py@@11@@OFF", 5000)
}
logInfo("Base Rules", "Frankie Solenoid Command Return Code: " + return_code)
end
rule "Ferdinand Set Temp Changed"
when
Item Target_Temp_Ferdinand changed
then
sendMail("email1@brewery.com; email2@brewery.com", "Ferdinand Set Temp Changed", "The set temperature on Ferdinand has changed to " + Target_Temp_Ferdinand.state.toString())
end
rule "Frankie Set Temp Changed"
when
Item Target_Temp_Frankie changed
then
sendMail("email1@brewery.com; email2@brewery.com", "Frankie Set Temp Changed", "The set temperature on Frankie has changed to " + Target_Temp_Frankie.state.toString())
end
rule "Francis Set Temp Changed"
when
Item Target_Temp_Francis changed
then
sendMail("email1@brewery.com; email2@brewery.com", "Francis Set Temp Changed", "The set temperature on Francis has changed to " + Target_Temp_Francis.state.toString())
end
Sitemap
The sitemap is where the defaults for target temperature are defined. Note the Switch items below with the presents for Crash, Ale and Hefe.
Screen.sitemap
sitemap screen label=""
{
Frame label="Temperatures" {
Text item=Temp_Fermenter_Ferdinand
Text item=Temp_Fermenter_Francis
Text item=Temp_Fermenter_Frankie
Text item=Temp_Glycol
}
Frame label="Target Temps" {
Switch item=Target_Temp_Ferdinand mappings=[1000="Off",37="Crash",69="Ale",72="Hefe"]
Switch item=Target_Temp_Francis mappings=[1000="Off",37="Crash",69="Ale",72="Hefe"]
Switch item=Target_Temp_Frankie mappings=[1000="Off",37="Crash",69="Ale",72="Hefe"]
}
}
Scripts
The following scripts are required for OpenHAB to monitor temperature data and control the solenoids:
get_probe_temp.sh
#!/bin/bash
temp=`grep -o t=\\-*[0-9][0-9]* /sys/bus/w1/devices/$1/w1_slave | sed s/t=//g | awk 'END {print $1/1000*9/5+32}'`
echo $temp
switch_solenoid.py
#!/usr/bin/python
import RPi.GPIO as GPIO
import time
import sys
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
pinList = [int(sys.argv[1])]
operation = sys.argv[2]
GPIO.setup(pinList[0], GPIO.OUT)
if operation == 'ON':
state = GPIO.HIGH
else:
state = GPIO.LOW
try:
GPIO.output(pinList[0], state)
#GPIO.cleanup()
sys.stdout.write('0')
except:
sys.stdout.write('100')
Conclusion
At this point, OpenHAB is up and running! I additionally used the midori browser and set it to start on boot, to display the OH sitemap at all times. There are a number of ways to do this, so I’ll leave it to you. The command I used to run midori is:
midori -e Fullscreen -a http://localhost:8080/openhab.app?sitemap=screen
Additional configuration changes I made included setting up SSL on OpenHAB, setting up user authentication, and allowing the OpenHAB app to connect to this install remotely. All of these steps can be found in the OpenHAB documentation.