Commercial Brewery Temperature Control with openHAB

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:

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.

8 Likes

Future readers of this post, please see the Migration Tutorial.

http://docs.openhab.org/tutorials/migration.html

There will be some minor changes required even with the compatibility layer, in particular, none of the imports are required (I don’t think the java.lang.* import was ever required).

It doesn’t look like it, but if any of the Rules are triggered by a Group, you will have to give your Group’s a Type for the Rule to continue to trigger.

@nolan_garrett, the formatting looks a little wonky. Did you use code fences? The Items look fine but the imports are missing from the Rules and the Rules and Sitemap are all mixed together.

```
code goes here
``` 

That should straighten that out.

Which OH 2 also does not, at the time of this posting, support. See the Security section of the User’s Guide for how to implement authorization and authentication using a reverse proxy like Nginx.

Thanks for posting! I love to see the unique ways users are putting OH to use.

Should be fixed now! Thank you!

1 Like

Cool project! There is just a little flaw: It lacks the correct spelling of “openHAB” :wink:

2 Likes