Brewing control?

This may be a bonkers idea but has anyone integrated ST with their beer brewing process / set up?

There are many solutions available such as the Electric Brewery (http://www.theelectricbrewery.com), Brewpi (http://www.brewpi.com), Brewtroller (https://brewtroller.com) and CraftBeerPi (http://www.craftbeerpi.com), however, each of these requires some dabbling with electrics and I would rather not…

In a nutshell, what is required is the ability to monitor a temperature and then turn on or off a thermostat or pump accordingly.

It seems to me that this ought to be easily achievable in the ST ecosystem so I’m going to have a stab.

I have a fibaro universal sensor (to measure temp) and will hook this up with an aeon gen 5 switch which ought to handle one 3000 W thermostat.

This ought to be easily doable using CORE (eg if temp below x then turn on / off switch).

However, it would be preferable to control the temp by a PID algorithm to maintain a constant temp (http://www.vandelogt.nl/uk_regelen_pid.php). Any idea whether anyone has already built into ST a PID algorithm?

Any pointers (to prevent me reinventing the wheel) would be appreciated. I have seen some old posts regarding cooling but nothing more recent.

The beauty of ST is that it ought to allow scaling up so if someone wanted to dabble with more powerful thermostats etc, this should still be possible.

2 Likes

I use it fairly simple manner. I have an ST multisensor apart and the cct board inside is mounted to my fermenter. I then have 3 rules in CoRE to regulate the heat via a heater wrapped on the fermenter. 2 rules for red/white/beer and one for temp overheat which shuts off the heater and SMS’s me if the temp rises and the rules fail to catch and turn off the heater.

1 Like

Quite a few people have done this in the past, but Core wasn’t available in the past, and now I think pretty much everybody would use that to control the rules. :sunglasses:

I know @jgirvine recently fitted out a “cheese cave” for homemade cheesemaking which has some similar environmental monitoring requirements, and might have more to add on device selection. :beers:

Anything that has the potential to aid in the production of quality craft brews cannot, by definition, be considered a “bonkers idea”…and you are to be commended for attempting to bring the latest technology available to bear on this most worthy pursuit.

Carry on, my good stalwart.

2 Likes

Hey, I am the one who setup a Cheese cave. I am able to monitor the temp and humidity inside it, as well as remotely monitor via SmartThings, . I use an Everspring Temp and Humidity controller inside the unit to alert me to drastic changes. https://www.amazon.com/Everspring-Z-Wave-Temperature-Humidity-ST814-2/dp/B00RYWBBU0. I use Core to send me a text if either goes out of range…
Here is a link to how I setup a cave: http://www.instructables.com/id/Cheese-Cave-From-a-Small-Refrigerator-or-Freezer/ The one thing I can not do is change the temp or humidity remotely…

3 Likes

This is also a noble endeavor.

Since I cannot in good conscience allow either of you to carry the full weight of such important work on your shoulders alone I shall selflessly volunteer my services as a quality assurance specialist. All you need do is ship representative samples of your finished products to my home address and I will review your work and provide you with the feedback that is critical to ensuring the gustatory perfection for which I know you are both striving. Note that initial test results are not always reliable, so multiple follow-up samples and reviews might be required.

3 Likes

I have used an iris contact sensor to monitor the ambient temp inside my home made “lagerator” when I do lagers. It reminds me to switch out the ice bath the carboy sits in. Used RM the last time I did a lager, will use Core the next time.

Thanks all

I will have a look at these - the downside to these forums is that there are so many ideas… I now want a cheese cave! Such a dangerous hobby but a great community…!

I’ll test out some ideas and report back. I am looking at temperature control for the mash rather than fermentation where even more precise control maintenance is required. That said, the same pid algorithm could be rolled out to both.

1 Like

Making progress - status so far - installed temp probes.

Much thanks to others for the DH and SA - I have only tweaked. Would be good to build all the functions of the Fibaro Universal Sensor into one DH and one SA but that is way beyond my skillset!


Part A – sort out the temperature probes

NOTE: THIS ASSUMES THAT YOU ARE USING CELSIUS TO RECORD YOUR TEMPERATURE READINGS. IF YOU ARE NOT, YOU MAY NEED TO CHANGE THE FORMULA IN THE DEVICE HANDLER TO GIVE YOU THE CORRECT RESULT. I WILL TRY TO CHECK THIS EVENTUALLY… ALSO NOTE THAT THE DOES NOT YET WORK WITH MINUS TEMPERATURES – AN EASY FIX JUST NOT YET DONE

Parts:

Fibaro universal sensor
12DC adaptor
4 x DS18B20 (http://www.ebay.co.uk/itm/111434870374 )

Step 1
A – removed ends of DC cable and soldered to PWR and GND of FUS (note: typically, wire with white strip on a DC adaptor is the positive wire which should join the PWR one)
B – soldered the four DS18B20 probes to the FUS (I had some spare wire which I soldered first to the FUS cables to extend them) – followed diagram4 at http://manuals.fibaro.com/content/manuals/en/FGBS-321/FGBS-321-EN-A-v1.00.pdf . You need to check the colour coding of your probes and match up with the manual. Ones I bought were red (VCC), black (GND and yellow (data)
C – should be ready to go – DO NOT TURN ON YET

Step 2 – link with SmartThings
A – go to add device (should automatically search)
B – turn on DC adaptor
C – press button on FUS three times relatively quickly
D – ST should recognise – call it what you want!

Step 3 – install custom device handler – note limitations above (possibly only Celsius and no negative temperatures)
A – login to IDE and install the following “from code” as a new device handler (apologies, I do not yet have a GH account – will set one up)

/**
 *  Device Type Definition File
 *
 *  Device Type:		Fibaro Universal Sensor - Dual Contact Sensor
 *  File Name:			Fibaro Universal Sensor - Dual Contact Sensor.groovy
 *	Initial Release:	2016-01-17
 *	Author:				Stuart Buchanan
 *  Modified:   Paul Crookes 25-01-2016
 *  Further modified by others 19-01-2017
 *
 *  Copyright 2016 Stuart Buchanan, based on original code by carlos.ir33 with thanks
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 ***************************************************************************************
 */
 
metadata {
	definition (name: "Fibaro Universal Sensor - Temperature sensors", namespace: "PukkaHQ", author: "Paul Crookes") {
    capability 	"Contact Sensor"
    capability 	"Temperature Measurement"
    capability 	"Configuration"
    
    attribute "temperature1", "number"
    attribute "temperature2", "number"
    attribute "temperature3", "number"
    attribute "temperature4", "number"
    
    
    command "listCurrentParams"
	
	fingerprint deviceId: "0x2001", inClusters: "0x30 0x60 0x85 0x8E 0x72 0x70 0x86 0x7A 0xEF"
}

simulator {
}

tiles {
// Removed because I am not using these - you may wish to keep included
	/* standardTile("contact1", "device.contact1", width: 1, height: 1) {
		state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
		state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
}
	standardTile("contact2", "device.contact2", width: 1, height: 1) {
		state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e"
		state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821"
}*/

        valueTile("temperature1", "device.temperature1", width: 1, height: 1) {
    state("temperature", label:'${currentValue}°',
        backgroundColors:[
            [value: 31, color: "#153591"],
            [value: 44, color: "#1e9cbb"],
            [value: 59, color: "#90d2a7"],
            [value: 74, color: "#44b621"],
            [value: 84, color: "#f1d801"],
            [value: 95, color: "#d04e00"],
            [value: 96, color: "#bc2323"]
        ]
    )
}

        valueTile("temperature2", "device.temperature2", width: 1, height: 1) {
    state("temperature", label:'${currentValue}°',
        backgroundColors:[
            [value: 31, color: "#153591"],
            [value: 44, color: "#1e9cbb"],
            [value: 59, color: "#90d2a7"],
            [value: 74, color: "#44b621"],
            [value: 84, color: "#f1d801"],
            [value: 95, color: "#d04e00"],
            [value: 96, color: "#bc2323"]
        ]
    )
}

        valueTile("temperature3", "device.temperature3", width: 1, height: 1) {
    state("temperature", label:'${currentValue}°',
        backgroundColors:[
            [value: 31, color: "#153591"],
            [value: 44, color: "#1e9cbb"],
            [value: 59, color: "#90d2a7"],
            [value: 74, color: "#44b621"],
            [value: 84, color: "#f1d801"],
            [value: 95, color: "#d04e00"],
            [value: 96, color: "#bc2323"]
        ]
    )
}

        valueTile("temperature4", "device.temperature4", width: 1, height: 1) {
    state("temperature", label:'${currentValue}°',
        backgroundColors:[
            [value: 31, color: "#153591"],
            [value: 44, color: "#1e9cbb"],
            [value: 59, color: "#90d2a7"],
            [value: 74, color: "#44b621"],
            [value: 84, color: "#f1d801"],
            [value: 95, color: "#d04e00"],
            [value: 96, color: "#bc2323"]
        ]
    )
}

standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") {
	state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure"
}

main([ "temperature1", "temperature2", "temperature3", "temperature4"])
details(["contact1","contact2", "temperature1", "temperature2", "temperature3", "temperature4", "configure"])
}
}

def parse(String description) {
	log.debug description
    def result = null
	def cmd = zwave.parse(description, [ 0x60: 3])
	if (cmd) {
		result = zwaveEvent(cmd)
	}
	log.debug "parsed '$description' to result: ${result}"
	result
}

def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv1.ManufacturerSpecificReport cmd) {
	log.debug("ManufacturerSpecificReport ${cmd.inspect()}")
}

def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) {
	log.debug("ConfigurationReport ${cmd.inspect()}")
}

def configure() {
	log.debug "configure"
    def cmds = []
	cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format()
	cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format()
	cmds << zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format()
	cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format()
	cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 4, size: 1).format()

    delayBetween(cmds, 500)
	}


def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) {
	log.debug "BasicSet V1 ${cmd.inspect()}"
	if (cmd.value) {
	createEvent(name: "contact1", value: "open", descriptionText: "$device.displayName is open")
	} else {
	createEvent(name: "contact1", value: "closed", descriptionText: "$device.displayName is closed")
	}
}

def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) {
	log.debug "ZWaveEvent V3 ${cmd.inspect()}"
	def result
    
    if (cmd.commandClass == 49) {
			if (cmd.sourceEndPoint == 3) {
            
               def brvalues = cmd.parameter
             def TempCalc = ((brvalues[4] * 256) + brvalues[5]) / 100
               
               log.debug TempCalc
             
               log.debug "Temp1"
               def map = [:]
               map.value = TempCalc
               map.displayed = true
               map.name = "temperature1"
               map.displayed = true
               result = createEvent(map)
			  
            }
			else
			if (cmd.sourceEndPoint == 4) {
            
               def brvalues = cmd.parameter
               def TempCalc = ((brvalues[4] * 256) + brvalues[5]) / 100
               def map = [:]
               map.value = TempCalc
               log.debug TempCalc
               map.name = "temperature2"
               map.displayed = true
               result = createEvent(map)
			   log.debug "Temp1 (2)"
				
			}
           
           	if (cmd.sourceEndPoint == 5) {
            
             def brvalues = cmd.parameter
               def TempCalc = ((brvalues[4] * 256) + brvalues[5]) / 100
               def map = [:]
               map.value = TempCalc
               log.debug TempCalc
               map.name = "temperature3"
               map.displayed = true
               result = createEvent(map)
			   log.debug "Temp1 (3)"
				
			}
            
            	if (cmd.sourceEndPoint == 6) {
                
               def brvalues = cmd.parameter
               def TempCalc = ((brvalues[4] * 256) + brvalues[5]) / 100
               def map = [:]
               map.value = TempCalc
               log.debug TempCalc
               map.name = "temperature4"
               map.displayed = true
               result = createEvent(name: "temperature4", value: TempCalc, descriptionText: "Temperature", temperature: TempCalc, precision: 2, scale: 0, scaledSensorValue: TempCalc, sensorType: 1, sensorValue: brvalues, size: 4)
			   log.debug "Temp1 (4)"
				
			}
    }
	if (cmd.commandClass == 32) {
		if (cmd.parameter == [0]) {
			if (cmd.sourceEndPoint == 1) {
				result = createEvent(name: "contact1", value: "closed", descriptionText: "$device.displayName is closed")
				log.debug "Contact1 is closed"
			}
			else
			if (cmd.sourceEndPoint == 2) {
				result = createEvent(name: "contact2", value: "closed", descriptionText: "$device.displayName is closed")
				log.debug "Contact2 is closed"
			}
		}
		if (cmd.parameter == [255]) {
			if (cmd.sourceEndPoint == 1) {
				result = createEvent(name: "contact1", value: "open", descriptionText: "$device.displayName is open")
				log.debug "Contact1 is open"
			}
			else
			if (cmd.sourceEndPoint == 2) {
				result = createEvent(name: "contact2", value: "open", descriptionText: "$device.displayName is open")
				log.debug "Contact2 is open"
			}
		}
	}
	return result
}

def zwaveEvent(physicalgraph.zwave.Command cmd) {
	// This will capture any commands not handled by other instances of zwaveEvent
	// and is recommended for development so you can see every command the device sends
	return createEvent(descriptionText: "${device.displayName}: ${cmd}")
}

def listCurrentParams() {
	log.debug "Listing of current parameter settings of ${device.displayName}"
    def cmds = []
	cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier:2).format()
	cmds << zwave.associationV2.associationGet(groupingIdentifier: 3).format()
	cmds << zwave.associationV1.associationGet(groupingIdentifier: 1).format()
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 3).format()
    cmds << zwave.configurationV1.configurationGet(parameterNumber: 4).format()
	
	delayBetween(cmds, 500)
}

B – go to devices and change the DH for the device you step up at step 2 to be this new custom one

Step 4 – create four “simulated temperature sensors”
A – go to “devices” in the IDE and create four new “simulated temperature sensors” – you can call these what you want

Step 5 – install the custom smartapp
A – login to IDE and install the following “from code” as a new device handler (again apologies, I do not yet have a GH account – will set one up)

/**
 *  Fibaro Universal Sensor App
 *
 *  Copyright 2014 Joel Tamkin
 *
 *	2015-10-29: erocm123 - I removed the scheduled refreshes for my Philio PAN04 as it supports instant
 *	status updates with my custom device type
 *  20016-01-25 PukkaHQ - Modified to work with Fibaro Universal Sensor
 *  2017-01-19 Further modified by others to work with temperature sensors from the FUS - removed compatibility with FUS contacts
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 */
definition(
    name: "Fibaro Univeral Sensor App",
    namespace: "",
    author: "Paul Crookes", // and others
    description: "Associates Fibaro Universal Sensor 4 Temperature Probes with four SmartThings simulated temperature sensors",
    category: "My Apps",
    iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
    iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")


preferences {
  section("FUS Module:") {
    input "fus", "capability.temperatureMeasurement", title: "Which FUS Module?", multiple: false, required: true
    input "temperature1", "capability.temperatureMeasurement", title: "First temp probe?", multiple: false, required: true
    input "temperature2", "capability.temperatureMeasurement", title: "Second temp probe?", multiple: false, required: true
     input "temperature3", "capability.temperatureMeasurement", title: "Third temp probe?", multiple: false, required: true
    input "temperature4", "capability.temperatureMeasurement", title: "Fourth temp probe?", multiple: false, required: true
  }
}

def installed() {
 log.debug "Installed with settings: ${settings}"
 initialize()
}

def updated() {
 log.debug "Updated with settings: ${settings}"
 unsubscribe()
 initialize()
}

/*def rsmHandler(evt) {

    def t1 = 0
    def t2 = 0
    def t3 = 0
    def t4 = 0
    
	log.debug "FUS RsMHandler"
    
    t1 = fus.currentValue("temperature1")
    t2 = fus.currentValue("temperature2")
    t3 = fus.currentValue("temperature3")
    t4 = fus.currentValue("temperature4")
    
    log.debug t1
    log.debug t2
    log.debug t3
    log.debug t4
        settings.temperature1.setTemperature(t1)
       settings.temperature2.setTemperature(t2)
       settings.temperature3.setTemperature(t3)
       settings.temperature4.setTemperature(t4)

   }*/

def rsmHandler1(evt) {

    def t1 = 0
    log.debug "FUS RsMHandler1"
    t1 = fus.currentValue("temperature1")
    log.debug t1
    settings.temperature1.setTemperature(t1)
    
    }
    
def rsmHandler2(evt) {

    def t2 = 0
    log.debug "FUS RsMHandler2"
    t2 = fus.currentValue("temperature2")
    log.debug t2
    settings.temperature2.setTemperature(t2)
    
    }    

def rsmHandler3(evt) {

    def t3 = 0
    log.debug "FUS RsMHandler3"
    t3 = fus.currentValue("temperature3")
    log.debug t3
    settings.temperature3.setTemperature(t3)
    
    }

def rsmHandler4(evt) {

    def t4 = 0
    log.debug "FUS RsMHandler4"
    t4 = fus.currentValue("temperature4")
    log.debug t4
    settings.temperature4.setTemperature(t4)
    
    }

def initialize() {
 subscribe(fus, "temperature1", rsmHandler1)
 subscribe(fus, "temperature2", rsmHandler2)
  subscribe(fus, "temperature3", rsmHandler3)
 subscribe(fus, "temperature4", rsmHandler4)
 /*These are for testing if events not triggering
 runEvery5Minutes(testhandler("temperature1"))
 schedule("23 20/2 * * * ?", testhandler ("temperature4"))
 unschedule()*/
}

Step 6 – activate the smartapp
A – go to marketplace in the mobile app
B – go to the smartapp tab and click “my apps”
C – this should allow you to install the fibaro universal sensor app (the step 5 app) – click on it and set the FUS module to the name in step 2(D) and the temperature sensors should be the ones in step 4
D – fingers crossed – you should be ready to go. Once all installed, try grasping one probe at a time tightly and see if the temperature in the app updates – should do so relatively quickly

Part B – setting up the proportional–integral–derivative controller smartapp
Parts
A – To be continued…

Various other stainless steel bits and bobs

Thermowells x 3 (http://www.ebay.co.uk/itm/252560247341)

3 Likes

Thank you for this, i’ve been messing around with this for a while & this is just what i was trying to achieve.

Glad it works. I still have not updated the temperature readings for minus temperatures so just keep that in mind.

The brewing control is still in development but I keep getting side-tracked!

This is exactly what I have been looking for. But how do i get this into MY smartthings eco system? I don’t see af link to any IDE on the new developer site: https://smartthings.developer.samsung.com/

Any help is appreciated!

top job thanks

Can you share some experiences on this? I am thinking of making one myself…,eit Fibaro Universal Sensor and Fibaro switch 2 (for controlling hear and cool).

I have not made a great amount of progress given other distractions.

Where I got to on the coding was to implement a number of other bits which were designed to maintain a steady mash / boil temperature.

I was in the process of testing this on a “live” / real world basis when I got sidetracked.

I will dig out the code / steps and share it.

One limitation was that there is (or was - I don’t know if ST has changed this) a limitation as to how often you could call routines (eg to check the temperature). That said, this did not seem to cause any issues when I was testing (eg checking temperature every 30 seconds instead of every second).

If you require minus temps to be monitored, someone developed a DH for the fibaro which works very nicely. I have not yet tried to extend that app to this.

If we could get a community effort to continue with this that would be fantastic! There is still much potential.

As above, I was aiming for an “electric brewery” type set up.

I do not like dabbling in electrics, so what I wanted to do was create a system where for the mashing and boiling, the system would turn the heating elements on and off as required to maintain a consistent temperature.

In my test set up, I plugged the heating element into an Aeotec smart plug (https://www.vesternet.com/z-wave-aeon-labs-smart-switch-6-gen5-uk) which is rated up to 3kw. The nice thing about this is that there is nothing to stop additional heating elements being added to additional plugs or (for someone willing to dabble in electrics) a more hard-wired solution.

Where I got to in testing was I had a boiler with a thermowell at the bottom (see above). The water flowed out of this at the bottom (drawn by a pump - not hooked up to ST) through a tube and back into the top of the boiling vessel.

The software I created effectively measured the temperature of the water as it left the vessel (through the thermowell) and turned the aeotec plug on or off depending on where the temperature stood - the broad idea being to hit and maintain a temperature.

There are a couple of variables in the equation which need to be calibrated (I will try to dig this out if I can find the paperwork) and once this has been done, the system hits and maintains a temperature pretty steadily.

The next stage would have been to hook up a suitable HERMS system and to experiment with that; that said, if it is possible to maintain a suitable temperature in the boil vessel as per the above, all that should be required is to add the HERMS system without any tinkering.

Code for this:

  1. I created three devices (i) hot liquor element (the aeotec plug - this is the standard DH), (ii) “hot liquor tank” temperature set, and (iii) “hot liquor tank” switch.

  2. “HLT” temperature set is a custom DH (I think…):

    /**

    • Copyright 2014 SmartThings
    • Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except
    • in compliance with the License. You may obtain a copy of the License at:
    •  http://www.apache.org/licenses/LICENSE-2.0
      
    • Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
    • on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
    • for the specific language governing permissions and limitations under the License.

    */
    metadata {
    // Automatically generated. Make future change here.
    definition (name: “HLT temp set”, namespace: “smartthings/testing”, author: “SmartThings”) {
    capability “Temperature Measurement”
    capability “Switch Level”
    capability “Sensor”

     	command "up"
     	command "down"
         command "setTemperature", ["number"]
         command "setone"
         command "settwo"
         command "setthree"
     }
    
    
     // UI tile definitions
     tiles {
     	valueTile("temperature", "device.temperature", width: 2, height: 2) {
     		state("temperature", label:'${currentValue}', unit:"C",
     			backgroundColors:[
     				[value: 31, color: "#153591"],
     				[value: 44, color: "#1e9cbb"],
     				[value: 59, color: "#90d2a7"],
     				[value: 74, color: "#44b621"],
     				[value: 84, color: "#f1d801"],
     				[value: 95, color: "#d04e00"],
     				[value: 96, color: "#bc2323"]
     			]
     		)
     	}
     	standardTile("up", "device.temperature", inactiveLabel: false, decoration: "flat") {
     		state "default", label:'up', action:"up"
     	}        
     	standardTile("down", "device.temperature", inactiveLabel: false, decoration: "flat") {
     		state "default", label:'down', action:"down"
     	}
         standardTile("setone", "device.temperature", inactiveLabel: false, decoration: "flat") {
     		state "default", label:'50', action:"setone"
     	}
         
         standardTile("settwo", "device.temperature", inactiveLabel: false, decoration: "flat") {
     		state "default", label:'67', action:"settwo"
     	}
         
         standardTile("setthree", "device.temperature", inactiveLabel: false, decoration: "flat") {
     		state "default", label:'70', action:"setthree"
     	}
         
         main "temperature"
     	details("temperature","up","down", "setone", "settwo", "setthree")
     }
    

    }

    // Parse incoming device messages to generate events
    def parse(String description) {
    def pair = description.split(":")
    createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:“C”)
    }

    def setLevel(value) {
    sendEvent(name:“temperature”, value: value)
    }

    def up() {
    def ts = device.currentState(“temperature”)
    def value = ts.doubleValue + 0.5 //? ts.integerValue + 0.5 : 72
    sendEvent(name:“temperature”, value: value)
    }

    def down() {
    def ts = device.currentState(“temperature”)
    def value = ts.doubleValue - 0.5 //? ts.integerValue - 0.5 : 72
    sendEvent(name:“temperature”, value: value)
    }

    def setone() {
    //def ts = device.currentState(“temperature”)
    //def value = ts.doubleValue - 0.5 //? ts.integerValue - 0.5 : 72
    sendEvent(name:“temperature”, value: 50)
    }
    def settwo() {
    //def ts = device.currentState(“temperature”)
    //def value = ts.doubleValue - 0.5 //? ts.integerValue - 0.5 : 72
    sendEvent(name:“temperature”, value: 67)
    }
    def setthree() {
    //def ts = device.currentState(“temperature”)
    //def value = ts.doubleValue - 0.5 //? ts.integerValue - 0.5 : 72
    sendEvent(name:“temperature”, value: 70)
    }

    def setTemperature(value) {
    sendEvent(name:“temperature”, value: value)
    }

  3. “HLT” switch is just a simulated switch which turns the program on or off.

  4. The smartapp which makes this work is the following:

     /**
      *  HLTpid
      *
      *  Copyright 2017 AJT
      *
      *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
      *  in compliance with the License. You may obtain a copy of the License at:
      *
      *      http://www.apache.org/licenses/LICENSE-2.0
      *
      *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
      *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
      *  for the specific language governing permissions and limitations under the License.
      *
      */
     definition(
         name: "HLTpidv2",
         namespace: "WBMST",
         author: "AJT",
         description: "Pid controller for brewing",
         category: "Convenience",
         iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
         iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
         iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png")
    
    
     preferences {
     	section("Title") {
     		// TODO: put inputs here
             input "theswitch", "capability.switch", title: "Virtual on switch", multiple: false, required: true
             input "poweron", "capability.switch", title: "Power switch (heating element)", multiple: false, required: true
             input "xk", "capability.temperatureMeasurement", title: "Live HLT temp", multiple: false, required: true
             input "tset", "capability.temperatureMeasurement", title: "Set temp", multiple: false, required: true
             input "kc", decimal, title: "Value of kc", multiple: false, required: true
             input "k0", decimal, title: "Value of k0", multiple: false, required: true
             input "k1", decimal, title: "Value of k1", multiple: false, required: true
     	}
     }
    
     def installed() {
     	log.debug "Installed with settings: ${settings}"
    
     	initialize()
     }
    
     def updated() {
     	log.debug "Updated with settings: ${settings}"
    
     	unsubscribe()
     	initialize()
     }
    
     def initialize() {
     	// TODO: subscribe to attributes, devices, locations, etc.
         
         state.yk = 0
         state.xk2 = 0
         state.xk1 = 0
         
     subscribe (theswitch, "switch.on", onHandler)
     subscribe (theswitch, "switch.off", offHandler)
     }
    
     // TODO: implement event handlers
     def onHandler (evt){
       log.debug "SCHEDULE ON"
       state.yk = 0
       state.xk2 = 0
       state.xk1 = 0
       
       //log.debug "prev temp = ${state.xk1}, prev temp2 = ${state.xk2}, yk = ${state.yk}"
       
       schedule ("0 0/1 * * * ?", HLTPID)
     }
    
     def offHandler (evt) {
       log.debug "SCHEDULE OFF"
       unschedule ()
       poweron.off()
       //CLEAR VALUES IN STATE
       state.yk = 0
       state.xk1 = 0
       state.xk2 = 0
       
       
       
     }
    
     def HLTPID (){
        
        log.debug "Current temp = ${xk.currentValue("temperature")}, kc = ${kc}, k0 = ${k0}, k1 = ${k1}"   
        log.debug "Current temp = ${xk.currentValue("temperature")}, prev temp = ${state.xk1}, prev temp2 = ${state.xk2}"
    
        def res1 = state.xk1 - xk.currentValue("temperature")
        
        def pp = 0
        
        //log.debug "res1 = ${res1}"
        //log.debug Math.abs(res1)
        
        if (res1 >= 0){
          //  log.debug ("Else1")   
            pp = kc.toBigDecimal() * res1 //(state.xk1 - xk.currentValue("temperature"))
        
        } else if (res1 < 0){
            //log.debug ("Else2")
            def pp1 = Math.abs(res1)
            //log.debug "pp1 = ${pp1}"
            pp = kc.toBigDecimal() * pp1
            //log.debug "pp = ${pp}"
            pp = -pp
        //    log.debug "pp = ${pp}"
        }
        
        //log.debug "res1 = ${res1}, pp = ${pp}"
        
        /*def res2 = tset.currentValue("temperature") - xk.currentValue("temperature")
        def pi = 0
    
        if (res2 >= 0 ){
        
        } else if (res2 < 0){
        
        }*/
        
        
        def pi = k0.toBigDecimal() * (tset.currentValue("temperature") - xk.currentValue("temperature"))
        def pd = k1.toBigDecimal() * ((2 * state.xk1) - xk.currentValue("temperature") - state.xk2)
        
    
           
        log.debug "pp = ${pp}, pi = ${pi}, pd = ${pd}"
        
        //It may be that should not add yk to this - tbd
        state.yk = state.yk + pp + pi + pd
        
        log.debug "Current yk = ${state.yk}"
        
        state.xk2 = state.xk1
        state.xk1 = xk.currentValue("temperature")
        
        if (state.yk > 100){
            state.yk = 100
        } else if (state.yk < 0){
            state.yk = 0
        }
    
         def turnoffinYsec = (state.yk / 100) * 59 //58 seconds so routine run before schedule
         log.debug("turnoffinYsec = ${turnoffinYsec}")
         
         if (turnoffinYsec >= 1){
             
         turnoffinYsec = turnoffinYsec.toInteger()
         
            log.debug "T1 on - off in ${turnoffinYsec}"
            poweron.on()
            runIn (turnoffinYsec, HLTPIDOFF)
         
         } else {
         
         //turnoffinYsec = 1
         //do nothing
         
         //just in case
          poweron.off()
         
         }
         
    
    
     }
    
     def HLTPIDOFF (){
       log.debug "T2 off"
       poweron.off()
    
     }
    

You will see from the input bits in the smartapp, it requires the three DH referred to above. The other variables in the “input” section are the variables which I mentioned above which need to be calibrated.

I hope this gives some guidance if anyone else is interested - I will get back on this eventually!

WBM

Here is the calibration page:

http://www.vandelogt.nl/uk_regelen_pid.php#PID3

It depends how much beer are you making, what temperature you holding and what components are you using. So, if you do not want to make a calculation mistake, you have to take all this stuff in consideration.
If we talking about equipment, i would suggest to use fabricated one special for brewing and to do not try to build it on your own while you just start doing it. Try to look at Craiglist or kegerator for sale to make a cheaper deal.
And good luck to you.

@WBM, I’m trying this out and I have a few questions. Hopefully you’re still using this.

  1. I’m looking at the calibration page you provided and I’m trying to relate that back to the three variables you have in the HLT code. The calibration page mentions kc, Ti, and Td and you have kc, k0, and k1. Can you tell me which variables from your code go to the variables in the calibration page?

  2. I have a separate temp set device handler that I’m planning on using (it uses deg F) and has a few other features. Will it hurt to mix your HLT smartapp that (deg C) with my temp set (deg F)?

Here’s a screenshot of a mock-up ActionTiles frontend to Smartthings. I’m hoping to make this happen soon.

Thanks

Thanks for getting in touch - I’m pleased that you are interested in this! Impressive screenshot!

I’ve not been working on this for a while but I calibrated these things. I’ll see if I can fine the instructions.

I have also slowly been migrating to Hubitat which has led me to parking things.

1 Like