Brewery control dashboard

I've been homebrewing 10 years, using Raspberry Pi to control an electric brewery for 6 of those. I've used several different programs in this time specific to homebrewing application (e.g. Strangebrew Elsinore, CraftBeerPi), and finally 2 years ago I decided to try Node-RED in conjunction with Node-RED-dashboard. I have built up a dashboard for temperature control using a cascade PID algorithm I wrote in a function node, pump control, manual element control, volume sensing, software based safety features (interlock to prevent certain element use when pump is not on, prevent multiple elements being used simultaneously, alerts and visual cues), and logging using influx db. I am currently working on scheduling functionality for step mashes!

This work can be found at jangevaare/brew2 on github and sometimes on my brewing blog


Hi Justin,
Your home brewery is well equipped! This reminds me of the days that my dad and I were also brewing beer. But to a lack of good material, we couldn't get the beer on the right temperatures fast enough. As a result we brewed one of the most worst Belgian beers ever :joy: But we had a lot of fun...
Nice to see such applications with a Node-RED system.
Thanks for sharing, and for refreshing my memories!


Thus is awesome mate, just what I've been looking for, to migrate from my aging craftbeerpi installation.
Installed modified and running perfectly so far for my 3 vessel 2 pump Herms system. Thanks for the great documentation.
Now to do a test run and retune the PIDs.

1 Like

Thanks @lauofdoom! Would love to be updated with how you get on with it and some of the changes you end up making - putting the modifications on your fork of the project on github are a great way of doing that too. I know others are using it as a basis for a 3 vessel set up.

This is awesome! I have a similar project that I'm just getting spun up and had two questions come immediately to mind. The first, 'what are you using for your PID control' I'll sort out by looking at the repo.

The second is how you're using InfluxDb. I'd like to be in a position to view a run after the fact. It seems pretty straight forward to send a copy of my dashboard feeds and possibly some timestamped node state info to a database, but I'm curious if there are any good mechanisms to view or replay that information after the fact in node-red, or if i just need to use a separate UI/app for that.

Might also be cool to pipe some historical information back through the process to do some anomaly detection and process tuning while it is in progress.

Thanks for the share, will definitely check it out!

For PID control I wrote my own function nodes to do that. I've included the code for the cascade PID below. Why did I write my own PID functions? Partly because I wanted to output all the internal calculations to help with my tuning. In addition to the custom functionality like output limiting and the cascade PID control for the mash control, and features to reduce integrator windup. I've also written PID algorithms before for other projects/pretty familiar with them so this was one of the less difficult parts of the project in ways.

I'm not doing any data visualization on my dashboard, I look at that afterwards using grafana for now, likely will switch to influxdb 2.0 when that is finalized.

The code below requires certain flow variables to be initialized...

// brew2 PID algorithm - integrators
// Justin Angevaare
// May 2018 - Nov. 2019

// Calculate outer integrator max
// Output to flow context
    flow.set('outer_integrator_max', (flow.get('outer_max')-flow.get('outer_min'))/flow.get('outer_i'))}else {
        flow.set('outer_integrator_max', 0.0)}

// Get current output max
// Output to flow context
    flow.set('inner_max', flow.get('inner_limit'))}else {
        flow.set('inner_max', 100)}

// Calculate inner integrator max
// Output to flow context
    flow.set('inner_integrator_max', flow.get('inner_max')/flow.get('inner_i'))}else {
        flow.set('inner_integrator_max', 0.0)}

return msg;

and the rest of this assumes that this function will be triggered every 2 seconds when running.

// brew2 Cascade PID algorithm
// Justin Angevaare
// Nov. 2019

// Manually specify update interval
msg.interval = 2000;

// Begin Outer Control Loop
// Output from Outer Control will be the target RIMS delta T
// Calculate outer error
msg.outer_target = flow.get('mash_target')
msg.outer_error = msg.outer_target - global.get('temp-MLT');

// Update outer integrator
// For purposes of integrator, bound error by [-1, 1]
msg.outer_integrator = flow.get('outer_integrator');
msg.outer_integrator += (msg.interval/1000) * Math.max(Math.min(msg.outer_error, 1), -1);

// Bound total outer integrator by absolute maximum
msg.outer_integrator = Math.max(Math.min(msg.outer_integrator, flow.get('outer_integrator_max')), -flow.get('outer_integrator_max'));

// Output updated integrator to flow context
flow.set('outer_integrator', msg.outer_integrator);

// Calculate proportional action
msg.outer_output = msg.outer_error * flow.get('outer_p');

// Calculate intergral action
msg.outer_output += msg.outer_integrator * flow.get('outer_i');

// Calculate derivative action
msg.outer_derivative = (msg.outer_error - flow.get('last_outer_error'))/(msg.interval/1000);
msg.outer_output += msg.outer_derivative * flow.get('outer_d');

// Bound outer_output by [outer_min, outer_max]
msg.outer_output = Math.max(Math.min(msg.outer_output, flow.get('outer_max')), flow.get('outer_min'));

// Set inner_target
msg.inner_target = Math.min(msg.outer_target + msg.outer_output, 90)

// Output updated last_outer_error to flow context
msg.last_outer_error = msg.outer_error;
flow.set('last_outer_error', msg.last_outer_error);

// Begin Inner Control Loop
// Output from inner control loop will be RIMS element duty %
// Calculate inner error
msg.inner_error = msg.inner_target - global.get('temp-RIMS');

// Update inner integrator
// For purposes of integrator, bound error by [-1, 1]
msg.inner_integrator = flow.get('inner_integrator');
msg.inner_integrator += (msg.interval/1000) * Math.max(Math.min(msg.inner_error, 1), -1);

// Bound total inner integrator by absolute maximum
msg.inner_integrator = Math.max(Math.min(msg.inner_integrator, flow.get('inner_integrator_max')), -flow.get('inner_integrator_max'));

// Output updated integrator to flow context
flow.set('inner_integrator', msg.inner_integrator);

// Calculate proportional action
msg.inner_output = msg.inner_error * flow.get('inner_p');

// Calculate intergral action
msg.inner_output += msg.inner_integrator * flow.get('inner_i');

// Calculate derivative action
msg.inner_derivative = (msg.inner_error - flow.get('last_inner_error'))/(msg.interval/1000);
msg.inner_output += msg.inner_derivative * flow.get('inner_d');

// Bound inner_output by [0.0, max_output]
msg.inner_output = Math.max(Math.min(msg.inner_output, flow.get('inner_max')), 0.0);

// Output updated last_inner_error to flow context
msg.last_inner_error = msg.inner_error;
flow.set('last_inner_error', msg.last_inner_error);

// Set payload to element output
msg.payload = msg.inner_output;

return msg;

First of all, I checked out your blog and the the pics of your brew station are really nice.
I love all the stainless!! Looks super clean

Yes! This is what I need as I don't really have a good intuition about PID tuning yet. I've been hitting YouTube recently to try to get a conceptual understanding, but when I'm watching things actually happen it's not always clear what is 'wrong' with the setup. Will take a gander at your code and see if i can incorporate.

Two quick hardware questions

First, how are you executing the proportional heat control on the high voltage side of your MLT while mashing? Right now I'm using a basic zero-crossing SSR. I originally tried triggering it with slow PWM but settled on node-red-contrib-t2abv's Bresenham node running at 125ms intervals. It seems to work well enough for now but would love to find a better option.

Second, are you using any pumps with proportional control? I'd like to find something with good flow control up to say 4L/min. Peristaltic pumps that size are pretty spendy (i don't need food safe). I found some PWM controlled cooling pumps for CPU coolers that i might try but again am interested in good options.


I am using zero crossing AC SSRs for the elements. I use them with a frequency of 1 or 2 seconds. I am just using the basic Raspberry Pi plugin for those outputs.

I have a 3 phase pump with a VFD. Hooking the VFD up to node red perhaps with modbus is on the long list of things I haven't yet gotten around to with this project.

Awesome, thanks for the info!

This topic was automatically closed after 60 days. New replies are no longer allowed.