PID controlled heater


New-ish to node red but have been using it in many applications and it has helped significantly reduce time to create a program with a simple UI that can be used in a lab environment. hopeing someone can help me on my latest project.

I am trying to heat a water bath. Currently in our lab we do this open loop with constant current but need more accuracy. I would like the user to be able to select temperature set points at given time(in seconds).

I'm looking for something similar to .

What i have done so far.
using the I have been storing data in arrays to plot the user defined ramp. how would I then overlay the thermocouple data onto that chart in "real time"?

I like the and would like use it if possible.

Below it the the flow I have been working on. hopefully someone can help me work through the logic.

[{"id":"5cd95dcc.6f0834","type":"tab","label":"Flow 2","disabled":false,"info":""},{"id":"548d5a70.00e424","type":"ui_chart","z":"5cd95dcc.6f0834","name":"","group":"e0aac8c4.bb9548","order":0,"width":"5","height":"5","label":"test 2","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"250","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#ff0000","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1550,"y":200,"wires":[[]]},{"id":"53938e72.6d4b","type":"inject","z":"5cd95dcc.6f0834","name":"delete graph","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[]","payloadType":"json","x":1010,"y":80,"wires":[["548d5a70.00e424"]]},{"id":"e873657e.634c28","type":"ui_numeric","z":"5cd95dcc.6f0834","name":"","label":"Start temp T-0 Seconds","tooltip":"","group":"e0aac8c4.bb9548","order":0,"width":0,"height":0,"wrap":false,"passthru":true,"topic":"","format":"{{value}}","min":0,"max":"200","step":1,"x":350,"y":120,"wires":[["76d1a2ff.bfcacc"]]},{"id":"87fc8f27.4353f","type":"ui_numeric","z":"5cd95dcc.6f0834","name":"","label":"Temp T-60 Seconds","tooltip":"","group":"e0aac8c4.bb9548","order":0,"width":0,"height":0,"wrap":false,"passthru":true,"topic":"","format":"{{value}}","min":0,"max":"200","step":1,"x":340,"y":160,"wires":[["c2878509.137488"]]},{"id":"96df0ac6.c71f28","type":"ui_numeric","z":"5cd95dcc.6f0834","name":"","label":"Temp T-120Seconds","tooltip":"","group":"e0aac8c4.bb9548","order":0,"width":0,"height":0,"wrap":false,"passthru":true,"topic":"","format":"{{value}}","min":0,"max":"200","step":1,"x":340,"y":200,"wires":[["d0518674.9d2278"]]},{"id":"7672d8e3.0f7b18","type":"function","z":"5cd95dcc.6f0834","name":"","func":"\nvar a = global.get('t0');\nvar b = global.get('t60');\nvar c = global.get('t120');\n\n\nvar m={};\nm.labels = [0,60,120];\nm.series = ['Programmed Value'];\ = [\n [a, b, c]\n ];\nreturn {payload:[m]};","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1240,"y":240,"wires":[["a9238c18.3fdec","548d5a70.00e424"]]},{"id":"76d1a2ff.bfcacc","type":"function","z":"5cd95dcc.6f0834","name":"global.set(\"t0\",msg.payload);","func":"global.set(\"t0\",msg.payload);","outputs":1,"noerr":0,"initialize":"","finalize":"","x":680,"y":120,"wires":[[]]},{"id":"c2878509.137488","type":"function","z":"5cd95dcc.6f0834","name":"global.set(\"t60\",msg.payload);","func":"global.set(\"t60\",msg.payload);","outputs":1,"noerr":0,"initialize":"","finalize":"","x":690,"y":160,"wires":[[]]},{"id":"d0518674.9d2278","type":"function","z":"5cd95dcc.6f0834","name":"global.set(\"t120\",msg.payload);","func":"global.set(\"t120\",msg.payload);","outputs":1,"noerr":0,"initialize":"","finalize":"","x":690,"y":200,"wires":[[]]},{"id":"c62a0121.f0bc7","type":"PID","z":"5cd95dcc.6f0834","name":"","setpoint":21,"pb":1,"ti":"0","td":0,"integral_default":0.5,"smooth_factor":3,"max_interval":600,"enable":1,"disabled_op":0,"x":1190,"y":320,"wires":[["885d58bc.192628"]]},{"id":"81a06ad0.57c588","type":"inject","z":"5cd95dcc.6f0834","name":"enable","repeat":"","crontab":"","once":false,"topic":"enable","payload":"true","payloadType":"bool","x":1030,"y":220,"wires":[["c62a0121.f0bc7"]]},{"id":"27bfeb99.489604","type":"inject","z":"5cd95dcc.6f0834","name":"disable","repeat":"","crontab":"","once":false,"topic":"enable","payload":"false","payloadType":"bool","x":1030.5,"y":270,"wires":[["c62a0121.f0bc7"]]},{"id":"a9238c18.3fdec","type":"debug","z":"5cd95dcc.6f0834","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1450,"y":100,"wires":[]},{"id":"885d58bc.192628","type":"range","z":"5cd95dcc.6f0834","minin":"0","maxin":"1","minout":"0","maxout":"100","action":"scale","round":false,"property":"payload","name":"","x":1460,"y":320,"wires":[[]]},{"id":"d774cecc.13a73","type":"inject","z":"5cd95dcc.6f0834","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1000,"y":180,"wires":[["7672d8e3.0f7b18"]]},{"id":"4b71556.63176ac","type":"function","z":"5cd95dcc.6f0834","name":"RampTimer","func":"var count= 0;\n\nvar counter=setInterval(timer, 1000); //1000 will run it every 1 second\n\n function timer()\n{\n count=count+1;\n if (count >= (120))\n {\n clearInterval(counter);\n //counter ended, do something here\n return;\n }\n \n msg.payload=count.toFixed(1);\n node.send(msg);\n global.set(\"RampTimer\",msg.payload);\n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":630,"y":240,"wires":[["f8cc254d.a07358"]]},{"id":"afb27665.fac4f8","type":"ui_button","z":"5cd95dcc.6f0834","name":"","group":"e0aac8c4.bb9548","order":5,"width":0,"height":0,"passthru":false,"label":"Start Ramp","tooltip":"","color":"","bgcolor":"","icon":"","payload":"true","payloadType":"bool","topic":"","x":310,"y":240,"wires":[["4b71556.63176ac"]]},{"id":"fb81b1a2.790ff","type":"ui_slider","z":"5cd95dcc.6f0834","name":"","label":"slider","tooltip":"","group":"e0aac8c4.bb9548","order":4,"width":0,"height":0,"passthru":true,"outs":"all","topic":"","min":0,"max":"50","step":1,"x":290,"y":280,"wires":[["9b39874b.d25d48"]]},{"id":"9b39874b.d25d48","type":"function","z":"5cd95dcc.6f0834","name":"global.set(\"Sim_Temp\",msg.payload);","func":"global.set(\"Sim_Temp\",msg.payload);","outputs":1,"noerr":0,"initialize":"","finalize":"","x":690,"y":280,"wires":[[]]},{"id":"f8cc254d.a07358","type":"function","z":"5cd95dcc.6f0834","name":"","func":"if(global.get(\"RampTimer\")<120){\n msg.payload=global.get(\"Sim_Temp\");\n node.send(msg); \n}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1020,"y":360,"wires":[["c62a0121.f0bc7"]]},{"id":"a9569f37.e7006","type":"function","z":"5cd95dcc.6f0834","name":"","func":"\nvar yearStart = 0;\nvar yearEnd = 120;\nvar counter = 0;\nvar a = global.get('Sim_Temp');\n\nvar Sim = {};\n\nif(yearEnd > counter+1){\n Sim.lables = [global.get('RampTimer')];\n Sim.series = ['Sim Value'];\n = [\n [a]\n ];\n counter++;\n}\n\nreturn {payload:[Sim]};\n","outputs":1,"noerr":0,"initialize":"","finalize":"","x":1220,"y":200,"wires":[["a9238c18.3fdec","548d5a70.00e424"]]},{"id":"f8584912.7c7cd8","type":"inject","z":"5cd95dcc.6f0834","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":1000,"y":140,"wires":[[]]},{"id":"e0aac8c4.bb9548","type":"ui_group","z":"","name":"uColumn Ramp","tab":"ae7a5799.808858","order":1,"disp":true,"width":"6","collapse":false},{"id":"ae7a5799.808858","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

Anyone have a good idea of how to set up the flow? I will be getting a thermocouple amplifier tomorrow so I can read temperature on a raspberry pi over SPI.

If you look back to near the beginning of the chart readme that you linked it says

You can also insert extra data points by specifying the timestamp property. This must be in either epoch time (in miliseconds, not seconds), or ISO8601 format.
{topic:"temperature", payload:22, timestamp:1520527095000}

so that should allow you to overlay the current temperature onto your chart, though I have not used that feature.

Are you ok with using the pid node? I can't look at your flow at the moment. How are you controlling the heater?

@Colin thanks for your feedback! I did see that feature but have not use epoch timestamps before. I will have to play with that feature.

The PID node looks great and would like use it. The PID node outputs a binary 0 or 1 that I can use to trigger a solid state relay connected to the rPi GPIO. The heater is a bunch of DC heater cartridges running at 24v.

I will probably post updated code tomorrow. Once I have time to hook everything up and start playing with the flow.

It is easy, if you already have the temperature in msg.payload and the topic in msg.topic then in a function node you can do

msg.timestamp = (new Date()).getTime()
return msg

to add the timestamp in milliseconds.

Not quite right, it outputs a linear value in the range 0 to 1 to indicate how much power should be applied. If you have an On/Off heater then have a look at node-red-contrib-timeprop that is designed for use with the pid node and will give you an easily configurable time proportioned output you can drive the SSR with.

Add the PID output (and the timeprop output if you wish) into the chart, that is invaluable for tuning the process. I suggest starting off with just a fixed setpoint so that you can get the tuning sorted before adding the profile. You can take them off again later once it is all working, or maybe just add a second chart for the moment, just showing the live data and the current timestamp. Then you can disable that once it is all going, and can re-enable it again later if you find you need to tweak something.

@Colin thanks agian! nice catch on the PID loop output I had fed it a very crappy simulation that forced it to be a binary output due to the extreme overshoots. node-red-contrib-timeprop looks like a perfect node to make it work. Hopefully I get hardware today and I can start getting stuff working!

Update on the project. I started playing with the thermocouple and the thermocouple amplifier. As @Colin suggested node-red-contrib-timeprop works great with the PID node. currently I am doing some tuning then I will work on user defined ramps and graphs. The code is below for anyone interested.

[{"id":"74f91b50.a099c4","type":"tab","label":"Flow 2","disabled":false,"info":""},{"id":"deae283b.abb9f8","type":"ui_chart","z":"74f91b50.a099c4","name":"","group":"cf1b24f4.be83c8","order":0,"width":"12","height":"10","label":"test 2","chartType":"line","legend":"true","xformat":"auto","interpolate":"linear","nodata":"","dot":false,"ymin":"25","ymax":"85","removeOlder":"100","removeOlderPoints":"","removeOlderUnit":"1","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#ff0000","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":1170,"y":60,"wires":[[]]},{"id":"59ad1117.8c652","type":"inject","z":"74f91b50.a099c4","name":"delete graph","repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[]","payloadType":"json","x":370,"y":60,"wires":[["deae283b.abb9f8"]]},{"id":"1a534b3b.860fb5","type":"PID","z":"74f91b50.a099c4","name":"","setpoint":21,"pb":"60","ti":"5","td":"25","integral_default":0.5,"smooth_factor":"3","max_interval":".01","enable":1,"disabled_op":0,"x":790,"y":200,"wires":[["d5a3f26d.b21c1","718ed76f.c51b48"]]},{"id":"ea301f53.b298e","type":"inject","z":"74f91b50.a099c4","name":"enable","repeat":"","crontab":"","once":false,"topic":"enable","payload":"true","payloadType":"bool","x":350,"y":180,"wires":[["1a534b3b.860fb5"]]},{"id":"bdd755b6.4f3f48","type":"inject","z":"74f91b50.a099c4","name":"disable","repeat":"","crontab":"","once":false,"topic":"enable","payload":"false","payloadType":"bool","x":350,"y":220,"wires":[["1a534b3b.860fb5"]]},{"id":"d5a3f26d.b21c1","type":"timeprop","z":"74f91b50.a099c4","name":"","cycleTime":".01","deadTime":0,"triggerPeriod":".0001","invert":false,"x":980,"y":200,"wires":[["77186146.50a44"]]},{"id":"4f12449f.32165c","type":"inject","z":"74f91b50.a099c4","name":"Setpoint 75","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"setpoint","payload":"75","payloadType":"num","x":370,"y":140,"wires":[["1a534b3b.860fb5"]]},{"id":"f05004d5.760f18","type":"inject","z":"74f91b50.a099c4","name":"Setpoint 30","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":"","topic":"setpoint","payload":"30","payloadType":"num","x":370,"y":100,"wires":[["1a534b3b.860fb5"]]},{"id":"77186146.50a44","type":"rpi-gpio out","z":"74f91b50.a099c4","name":"","pin":"11","set":"","level":"0","freq":"","out":"out","x":1180,"y":180,"wires":[]},{"id":"56800358.ae70bc","type":"serial in","z":"74f91b50.a099c4","name":"","serial":"bca839cb.ac5688","x":350,"y":420,"wires":[["deae283b.abb9f8","1a534b3b.860fb5","df523cf6.1dfb7"]]},{"id":"df523cf6.1dfb7","type":"ui_gauge","z":"74f91b50.a099c4","name":"","group":"cf1b24f4.be83c8","order":2,"width":0,"height":0,"gtype":"gage","title":"gauge","label":"units","format":"{{value}}","min":0,"max":"100","colors":["#00b500","#e6e600","#ca3838"],"seg1":"","seg2":"","x":650,"y":420,"wires":[]},{"id":"266f8ee4.fa2592","type":"ui_numeric","z":"74f91b50.a099c4","name":"","label":"P-Term","tooltip":"","group":"cf1b24f4.be83c8","order":3,"width":0,"height":0,"wrap":false,"passthru":true,"topic":"prop_band","format":"{{value}}","min":0,"max":"1","step":"0.001","x":340,"y":260,"wires":[["1a534b3b.860fb5"]]},{"id":"933585cc.b0bea8","type":"ui_numeric","z":"74f91b50.a099c4","name":"","label":"I-Term","tooltip":"","group":"cf1b24f4.be83c8","order":3,"width":0,"height":0,"wrap":false,"passthru":true,"topic":"t_integral","format":"{{value}}","min":0,"max":"1","step":".01","x":330,"y":300,"wires":[["1a534b3b.860fb5"]]},{"id":"be612735.acd788","type":"ui_numeric","z":"74f91b50.a099c4","name":"","label":"D-Term","tooltip":"","group":"cf1b24f4.be83c8","order":3,"width":0,"height":0,"wrap":false,"passthru":true,"topic":"t_derivative","format":"{{value}}","min":0,"max":"2","step":".01","x":340,"y":340,"wires":[["1a534b3b.860fb5"]]},{"id":"94387bbf.1a0478","type":"inject","z":"74f91b50.a099c4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"prop_band","payload":".01","payloadType":"num","x":100,"y":260,"wires":[["266f8ee4.fa2592"]]},{"id":"ac6d9561.516298","type":"inject","z":"74f91b50.a099c4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"t_integral","payload":".1","payloadType":"num","x":90,"y":300,"wires":[["933585cc.b0bea8"]]},{"id":"8dd4d550.4598a8","type":"inject","z":"74f91b50.a099c4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"t_derivative","payload":".1","payloadType":"num","x":100,"y":340,"wires":[["be612735.acd788"]]},{"id":"718ed76f.c51b48","type":"ui_chart","z":"74f91b50.a099c4","name":"","group":"cf1b24f4.be83c8","order":0,"width":"12","height":"10","label":"PID","chartType":"line","legend":"true","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"1","removeOlder":"25","removeOlderPoints":"","removeOlderUnit":"1","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#ff0000","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"useOldStyle":false,"outputs":1,"x":990,"y":340,"wires":[[]]},{"id":"7bc48198.6c43c","type":"ui_numeric","z":"74f91b50.a099c4","name":"","label":"D-Term Smoothing","tooltip":"","group":"cf1b24f4.be83c8","order":3,"width":0,"height":0,"wrap":false,"passthru":true,"topic":"smooth_factor","format":"{{value}}","min":0,"max":"100","step":".1","x":370,"y":380,"wires":[["1a534b3b.860fb5"]]},{"id":"3bdb46fe.57766a","type":"inject","z":"74f91b50.a099c4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"smooth_factor","payload":"3","payloadType":"num","x":100,"y":380,"wires":[["7bc48198.6c43c"]]},{"id":"cf1b24f4.be83c8","type":"ui_group","z":"","name":"uColumn Ramp","tab":"c705970.1805b68","order":1,"disp":true,"width":"12","collapse":false},{"id":"bca839cb.ac5688","type":"serial-port","z":"","serialport":"/dev/ttyUSB0","serialbaud":"115200","databits":"8","parity":"none","stopbits":"1","waitfor":"","dtr":"none","rts":"none","cts":"none","dsr":"none","newline":"\\n","bin":"false","out":"char","addchar":"","responsetimeout":"10000"},{"id":"c705970.1805b68","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

Excellent. PID is almost magic when tuned isn't it.
Did you realise you only exported one node (the chart)?

Have a look at node-red-contrib-ramp-thermostat if you are doing profiles. It has some good ramp profile stuff and you can just ignore the thermostat bit.

@Colin oops! I edited the post to include the whole flow... I have some experience tuning PID loops on other projects. Tuning a PID loop for a heater with out active cooling is interesting because i have to set the p-gain very low because the thermal mass is very very small in my case.

The pid values you have in the node look a bit suspicious to me. I suspect you have the integral time to low and the PB much too wide. Also the Derivative time would normally be quite a bit smaller than the Integral time. I rather suspect it is pretty much operating in on/off mode. It is worth while to put the PID output on the chart too, at least whilst tuning, it will give you a lot of information about what is going on. I suspect you will find it is swinging about dramatically.

Could I suggest that you wind the Integral time up to a large value, say 1800 seconds, set the derivative to 0 and the PB down at about 10 and see what happens. It will take a long time to close in on the setpoint after the initial rise, but then you will be able to tune the PB by looking at the shape of the graph, bringing it down further till it starts to ring on startup.
Did you see my blog on tuning which will show what to expect as you bring the PB down?

I have just realised - are those numbers you have labelled on the chart P Term, I Term and D Term actually the proportional band (which is degrees), and the Derivative and Integral times, which are in seconds? If you have a proportional band of .01 degrees that means that you are using it as an on/off controller because once it gets more than that from the setpoint the heat will be fully on or off.
You may be confused if you have previously used the type of pid algorithm where you setup gain and integral and derivative factors. The parameters in node-red-contrib-pid and much easier to get a handle on because they are in real-world units and you can get a reasonable guess at what they should be just by looking at the startup response. The underlying maths is the same, it is just the parameter entry that is different.
Note that a large PB equates to a low gain, a large Integral time equates to a low integral effect (the time is large so it takes a long time to have any effect). With derivative however a large time equates to a large derivative effect. Hence my previous suggestion to start with it at zero for initial tuning. For a simple heating process you may not even need any D.

@Colin I had not found your blog on tuning. I will read it and work my way through the process! Thanks for the information!


When I set the prop-band=10, Integral-time=1800 and Derivative-time=0 i get a steady state offset of a few degrees. In the image below the set point is 30c but the actual temperature is ~33c. The PID grapgh does not seem to be letting the output drop low enough. That being said it is much more consistent temperature.

FYI the thermocouple temperature is at 10hz and so i set the time-prop "cycle time" to 0.01 and "trigger time" to 0.0001(1%)

The offset is because the integral is set to half an hour, that means the integral takes approx that long to halve the error between it and the setpoint. I think that is noted in the blog. For tuning the PB you should ignore the offset. Once the PB is approx right then the integral can be set, probably much shorter. Have you got a chart showiing the startup from cold to the (initial) steady state?
The graph would be better if both lines were on the same chart. You can feed the PID value through a range node to scale it for an input of 0 to 1 it gives an output of 20 to 40 so it will appear on the chart.

Just noticed your comments on the timeprop node. It only updates it's value once a second so a cycle time any less that 10 seconds doesn't make any sense. Looking at the chart earlier I would set it at about 60 seconds.

[Edit] Looking at the earlier chart I don't think you need sample the temperature any quicker than once a second. It doesn't change measurably in that time does it?

@Colin I scaled the PID from 0-1 to 0-100 and combined the graph as shown in image below. This heater has very low thermal mass so that is why I am running at 10hz. I dropped the integral time to 10sec and good the same steady state off-set as shown in the second images.

Currently is have the time-prop set to "cycle time" to 0.1 and "trigger time" to 0.001(1%). If I go to large cycle time I get a large bounce.

I was talking rubbish when I said the timeprop would not go that fast. I have also written versions of PID and Timeprop that run in the Tasmota s/w for for devices such as the Sonoff TH10 which has a low spec processor of course and it won't go anything like that fast. I have been working with one of those and had not switched back to node-red mode, where if you have sufficient processor power it will go as fast as you like. I completely misread the timescale on your first chart which is why I thought the process was much slower than it is.

Are you saying that it has stabilised away from the setpoint? If, with the integral on 10 seconds, you leave it a few minutes does it still not come in?

@Colin time-prop has no problem running a cycle time of 0.1s on the Rpi4 in this case.

The temperature never stabilizes on the set point, it is always 3-4 degreeC to high. it is always high regardless of integral time. I have tried integral times of 0,1,10,500 and 1800 no change. in this case the heaters heat very fast but they cool down very slow maybe the fact that its unbalance is hurting the PID loop. If I lower the PB down to 1.1 the temperature is roughly 0.5 degreeC high which is at least a little more accurate.

Any thoughts on how to do a linear ramp function. So the user inputs "max temp" and "ramp time" then have a "start ramp button". i started writing a java script function but haven't got it working yet. maybe there is a easier way.

If you put in very small integral then the power should very rapidly change to full on or off. Are you absolutely sure you are changing the integral? Try changing it in the node and see what happens (remove the wire from the dashboard node temporarily to make sure it is not doing something).
You should really have tuned the PB before worrying about integral. On the graph you posted with it set to 10 I see that it looks very damped which means that the pb is too large. The fact that you reduced it to 1.1 and it did not go unstable means that it was much too large.
If the temperature is above the setpoint then the power should reduce over time till it is zero or the temperature comes down, with the time being related to the integral time. With integral time 10 seconds then the power should come down close to zero within a minute if the temperature is significantly away from setpoint (compared to PB). That is why I wonder whether your integral setting flow is not working. Have you put a debug node on it and checked the topic and value are correct? If you can't see a problem then as I suggested, disconnect it for the moment and make the setting in the node.

It is late here so I am signing off for the moment. If you can't see anything wrong with the integral then if edit the file .node-red/node_modules/ node-red-contrib-pid then at about line 110 you will see

    function runControlLoop() {
      //node.log("pv, setpoint, prop_band, t_integral, t_derivative, integral_default, smooth_factor, max_interval, enable, disabled_op");
      //node.log(node.pv + " " + node.setpoint + " " + node.prop_band + " " + node.t_integral + " " + node.t_derivative + " " + node.integral_default + " " + node.smooth_factor + " " + node.max_interval + " " + node.enable + " " + node.disabled_op);

remove the comments (the two slashes) in front of the node.log lines and restart node-red. It will then log all the parameters to /var/log/syslog every time a new temperature is read, which will be a bit excessive. But you can check that the integral time is doing what you expect, and the others. Just run it long enough to get some values then stop node red and put the comments back. Being in syslog means you can go back and look at it after you have stopped node-red.