node-red-contrib-pid
node-red-contrib-pid copied to clipboard
PID setpoint tracking
Hi @colinl
The first to thank you for your excellent job!
I am designing a system at home where I need all photovoltaic production to be used to heat water.
What I need is, if the solar panels are generating 1kw (this measurement is extracted by mqtt from the inverter and it would be the setpoint remote) I need to divert all that power to a resistor controlled by a dimmer with 0-10v signal input through a Shelly Dimmer 0-10v
Regulator 25A https://es.aliexpress.com/item/20000003997748.html?spm=a2g0o.cart.0.0.eec37a9dSt4vWf&mp=1&gatewayAdapt=glo2esp
Shelly Dimmer 0-10V https://shelly-api-docs.shelly.cloud/gen2/Devices/ShellyPlus10V
SP is the instantaneous solar power extracted by MQTT from the solar inverter, logically the SP will be continuously oscillating according to the solar generation.
PV will be a ShellyEM with a clamp measuring the consumption of the dimmer. (This value can be obtained by Json HTTP)
OUT .Output will be sent to the Shelly Dimmer (this expects a range of 0-100%) and then in proportion it will act with 0-10v on the regulator where the resistance that heats the water is connected.
The ultimate goal is that the PV = SP
Do you think could do it with your node?
At the moment I'm trying this initial modified version of your example, but I'm getting the Integral Locked error. I imagine it is because I am not entering the PV value correctly in the PID block.
Greetings from Spain and sorry for my terrible English.
What do you see from debug 256 and 258? Set 258 to Output Complete Message and fully expand it in the debug pane. Integral locked is not an error, it is just telling you that the PV is a long way from the setpoint so the output will be clamped to 0 or 1, depending on which side of the setpoint it is. The default operation of the node is to assume that the process is a 'heating' type process, so the output will be 0 when the PV is a long way above the SP and 1 when it is a long way below the setpoint.
HI @colinl Thanks for your prompt response.
Debug256= (power in W range 0-2000W) It is the value that I want to enter into the PID in PV.
Debug258= PID OUT output value, to control a dimmer with 0-10v input. Sorry, I'm starting with nodered and I don't know Set 258 to Output Complete Message and fully expand it in the debug panel.
I have scaled the input signal to the PV PID 0-2000W to 0-1, and I have scaled the OUT from 0-1 to 0-100% (input to the ShellyDimmer, which will convert 0-100% to 0-10V which will serve to control the regulator)
I don't know if it is necessary to scale the PV input and OUT output of the PID.
Is there any way to know what values are in/out of the PID block?
I think the problem is that I am not knowing how to correctly connect the PV value (0-2000W) to the PID. and that's why it says it gives that "warning" of Integral Locked, because the SP is normally close to the PV.
In this case the loop works correctly in heating mode. If the PV is less than the SP, the OUT increases. It's right.
Greetings and Happy New Year!
As a beginner, I recommend watching this playlist: Node-RED Essentials. The videos are done by the developers of node-red. They're nice & short and to the point. You will understand a whole lot more in about 1 hour. A small investment for a lot of gain.
I don't know Set 258 to Output Complete Message and fully expand it in the debug panel.
To expand the data in the debug window, click against the values in the debug window and it will expand the objects. Keep clicking the contained objects till they are all shown. You should end up with something like this
Is there any way to know what values are in/out of the PID block?
That is what we will now see in the debug window.
Thanks @colinl
I have now been able to open the complete debug.
Do you think that the PID could respond well to even a remote SP that will be oscillating continuously.
The objective is for two signals to follow each other....
SP=power that the inverter is generating PV=power consumed by the heating resistance, and which must be equal to SP.
Another option... since the resistive heating element behaves in a "linear" way, you could simply make a range function.
Input 0-2000W and output 0-100% (knowing that at 50% the load consumes 1000W), it may have some deviation watts since the internal resistance of the heating element itself increases when it heats up, but it would respond very quickly and without having than tuning a PID. You could take the PV value just to monitor and verify.
What is your opinion?
I agree, you don't need a PID loop for this. For PID to work the sample rate needs to be significantly faster than the process responds, but when you change the dimmer setting the PV will change very rapidly. My approach would be to sample the current as quickly as you can, and at each sample compare the value with the inverter output. If, for example, it said that the PV is 10 units below the inverter output then I would increase the dimmer setting to take the current 3/4 of the way from where it is to the required value. If you try and take it all the way in one step then you might find the loop is unstable. Then you can tweak the 3/4 setting to get it to respond as quickly as you can without it becoming unstable.
Thanks Colin, I think it's a good idea to start trying.
So if I understood you wrong, we wouldn't use your PID node, right?
Could you please, when you have a little time, make an example json. I'm sure it will help me better understand how to do it.
Thank you again for your time and patience!
At what rate do you get a value from the current clamp?
Initially, the watt value to follow is obtained by mqtt every 5 seconds.
I meant the actual power going into the resistor, that is the PV value from the current clamp.
The PV value measured on the heating resistance clamp could be done at a maximum of 1 second.
I have changed my mind, using the PID node is how I would do it. I have built a test flow with a simulation of your dimmer and heater.
You should be able to Import the flow below and see how it works. It is pretty much what you had, except that I have added a 2 second low pass filter to help keep the loop stable. Sample the current as quickly as you can. Replace the simulation with your hardware and see how it goes. If the output hops up and down at each sample then increase the prob band. The integral time determines how quickly it homes in on the setpoint, but if you go below about 4 times the sample rate then it may go unstable. I have got it at 4 seconds which should be ok for 1 second sample rate. Reducing the prop band will bring it in quicker, but if you go too far it will go unstable. If you have to sample the PV at a slower rate then set the filter to twice the sample period, and the integral at four times it.
[{"id":"11eda06c2b800da1","type":"comment","z":"bdd7be38.d3b55","name":"Actual measured value in watts (PV)","info":"","x":160,"y":4480,"wires":[]},{"id":"65391c5cc1280bd8","type":"link in","z":"bdd7be38.d3b55","name":"link in 11","links":["86ea8967949d7710"],"x":335,"y":4480,"wires":[["a65a480287ee2764","a604f5227071c0a5"]]},{"id":"815d714116d29ea0","type":"comment","z":"bdd7be38.d3b55","name":"Required value in watts (SP)","info":"","x":140,"y":4300,"wires":[]},{"id":"86bb53abe66cb64b","type":"inject","z":"bdd7be38.d3b55","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setpoint","payload":"100","payloadType":"num","x":130,"y":4380,"wires":[["3886d6d4962287a5"]]},{"id":"a65a480287ee2764","type":"PID","z":"bdd7be38.d3b55","name":"","setpoint":"0","pb":"2000","ti":"4","td":0,"integral_default":"0","smooth_factor":3,"max_interval":600,"enable":1,"disabled_op":0,"x":530,"y":4380,"wires":[["3bce6d5b86ad11f7","e5f55ebe7cf034b2"]]},{"id":"cd8dac5283070133","type":"inject","z":"bdd7be38.d3b55","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setpoint","payload":"0","payloadType":"num","x":120,"y":4340,"wires":[["3886d6d4962287a5"]]},{"id":"b932d25908e735b7","type":"inject","z":"bdd7be38.d3b55","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setpoint","payload":"1000","payloadType":"num","x":130,"y":4420,"wires":[["3886d6d4962287a5"]]},{"id":"3bce6d5b86ad11f7","type":"range","z":"bdd7be38.d3b55","minin":"0","maxin":"1","minout":"0","maxout":"100","action":"clamp","round":false,"property":"payload","name":"Scale 0 to 100","x":750,"y":4380,"wires":[["df676488333ff807"]]},{"id":"99f38f0402b0410a","type":"link out","z":"bdd7be38.d3b55","name":"link out 28","mode":"link","links":["31e2d20c5433e56d"],"x":1145,"y":4380,"wires":[]},{"id":"31e2d20c5433e56d","type":"link in","z":"bdd7be38.d3b55","name":"link in 12","links":["99f38f0402b0410a"],"x":65,"y":4640,"wires":[["9d3ba56efb2ea143"]]},{"id":"86ea8967949d7710","type":"link out","z":"bdd7be38.d3b55","name":"link out 29","mode":"link","links":["65391c5cc1280bd8"],"x":1215,"y":4820,"wires":[]},{"id":"d375d9dfc3f8ba0f","type":"ui_chart","z":"bdd7be38.d3b55","name":"","group":"c45a83a3.d00908","order":10,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"2000","removeOlder":"5","removeOlderPoints":"","removeOlderUnit":"60","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"className":"","x":1090,"y":4480,"wires":[[]]},{"id":"a604f5227071c0a5","type":"change","z":"bdd7be38.d3b55","name":"topic: PV","rules":[{"t":"set","p":"topic","pt":"msg","to":"PV","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":500,"y":4480,"wires":[["d375d9dfc3f8ba0f"]]},{"id":"410cb8ff585e8ca0","type":"debug","z":"bdd7be38.d3b55","name":"Scaled PID Output","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1130,"y":4420,"wires":[]},{"id":"e5f55ebe7cf034b2","type":"change","z":"bdd7be38.d3b55","name":"topic: Demand","rules":[{"t":"set","p":"topic","pt":"msg","to":"Demand","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":720,"y":4440,"wires":[["516783e442a7f6ab"]]},{"id":"516783e442a7f6ab","type":"range","z":"bdd7be38.d3b55","minin":"0","maxin":"1","minout":"0","maxout":"500","action":"clamp","round":false,"property":"payload","name":"Scale 0 to 500","x":900,"y":4440,"wires":[["d375d9dfc3f8ba0f"]]},{"id":"4c7a801d8a07c326","type":"comment","z":"bdd7be38.d3b55","name":"Output to dimmer","info":"","x":1140,"y":4340,"wires":[]},{"id":"df676488333ff807","type":"function","z":"bdd7be38.d3b55","name":"2 sec RC","func":"// Applies a simple RC low pass filter to incoming payload values\nvar tc = 2*1000; // time constant in milliseconds\n\nvar lastValue = context.get('lastValue');\nif (typeof lastValue == \"undefined\") lastValue = msg.payload;\nvar lastTime = context.get('lastTime') || null;\nvar now = new Date();\nvar currentValue = msg.payload;\nif (lastTime === null) {\n // first time through\n newValue = currentValue;\n} else {\n var dt = now.getTime() - lastTime.getTime();\n var newValue;\n \n if (dt > 0) {\n var dtotc = dt / tc;\n newValue = lastValue * (1 - dtotc) + currentValue * dtotc;\n } else {\n // no time has elapsed leave output the same as last time\n newValue = lastValue;\n }\n}\ncontext.set('lastValue', newValue);\ncontext.set('lastTime', now);\n\nmsg.payload = newValue;\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":940,"y":4380,"wires":[["99f38f0402b0410a","410cb8ff585e8ca0"]]},{"id":"3886d6d4962287a5","type":"junction","z":"bdd7be38.d3b55","x":340,"y":4340,"wires":[["a65a480287ee2764"]]},{"id":"b9e51a3357199286","type":"group","z":"bdd7be38.d3b55","name":"Process Simulation","style":{"label":true},"nodes":["9d3ba56efb2ea143","8314d7341bc8260b","de22788849c4b1fa","73f72dd28cef81ed","6e6f1975900d627d","c5a1ce224d5b111b","9064309b1f74e79d","4cd86fd0004bd87f","cdf1bcaf43480cc9"],"x":94,"y":4559,"w":1052,"h":302},{"id":"9d3ba56efb2ea143","type":"range","z":"bdd7be38.d3b55","g":"b9e51a3357199286","minin":"0","maxin":"100","minout":"0","maxout":"2000","action":"clamp","round":false,"property":"payload","name":"Scale 0:100 to 0:2000","x":220,"y":4640,"wires":[["8314d7341bc8260b"]]},{"id":"8314d7341bc8260b","type":"delay","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"","pauseType":"delay","timeout":"100","timeoutUnits":"milliseconds","rate":"1","nbRateUnits":"1","rateUnits":"second","randomFirst":"1","randomLast":"5","randomUnits":"seconds","drop":false,"allowrate":false,"outputs":1,"x":410,"y":4640,"wires":[["de22788849c4b1fa"]]},{"id":"de22788849c4b1fa","type":"change","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"topic: input","rules":[{"t":"set","p":"topic","pt":"msg","to":"input","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":610,"y":4640,"wires":[["73f72dd28cef81ed"]]},{"id":"73f72dd28cef81ed","type":"join","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"","mode":"custom","build":"object","property":"payload","propertyType":"msg","key":"topic","joiner":"\\n","joinerType":"str","accumulate":true,"timeout":"","count":"2","reduceRight":false,"reduceExp":"","reduceInit":"","reduceInitType":"","reduceFixup":"","x":750,"y":4680,"wires":[["9064309b1f74e79d"]]},{"id":"6e6f1975900d627d","type":"inject","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":430,"y":4600,"wires":[["de22788849c4b1fa"]]},{"id":"c5a1ce224d5b111b","type":"inject","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"Trigger 1 sec","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":true,"onceDelay":0.1,"topic":"trigger","payload":"0","payloadType":"num","x":600,"y":4720,"wires":[["73f72dd28cef81ed"]]},{"id":"9064309b1f74e79d","type":"switch","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"Trigger?","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"trigger","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":600,"y":4820,"wires":[["4cd86fd0004bd87f"]]},{"id":"4cd86fd0004bd87f","type":"change","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"Move actual to payload","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.input","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":790,"y":4820,"wires":[["cdf1bcaf43480cc9"]]},{"id":"cdf1bcaf43480cc9","type":"function","z":"bdd7be38.d3b55","g":"b9e51a3357199286","name":"Clear other properties","func":"msg = {payload: msg.payload}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1020,"y":4820,"wires":[["86ea8967949d7710"]]},{"id":"c45a83a3.d00908","type":"ui_group","name":"PID","tab":"80cd4062.93a5","order":1,"disp":false,"width":"12","collapse":false},{"id":"80cd4062.93a5","type":"ui_tab","name":"PID","icon":"dashboard","disabled":false,"hidden":false}]
Ohhhhh @colinl thank you very much for this great work. I'm still waiting for the Shelly 0-10V dimmer to arrive from Aliexpress.
Looking forward to trying it....I have some questions.
Remember that the setpoint we have there is for testing or to make it work manually.
In NORMAL operation, the SP signal will be the Power generated by the solar inverter, and it will oscillate continuously.
Here is the question of how this PID will work, when a new SP is arriving approximately once per second.
Another question I have... why have you used a 0-500 scaling? It should be 0-2000W right?
Thanks again for your time!
SP signal will be the Power generated by the solar inverter, and it will oscillate continuously.
What do you mean by oscillate?
why have you used a 0-500 scaling
I didn't want the power chart line to take up the whole chart. It is easier to see what is going on if it only fills the bottom 1/4 of the chart.
I mean that the SP will change continuously depending on the solar radiation and the clouds, it is a measurement that is never stable. The SP will be the measure of power (watts) generated by the inverter.
I don't know if I can explain.
Thank you!
The pid node is designed to be able to track varying setpoints. Of course, since it takes a number of seconds to home in on a new value it will not track rapid changes quickly. It will do its best to follow the setpoint. You can see how it performs if you configure some inject nodes with the sort of values you expect and click them at an appropriate rate. The average power in the heater over a period should match the average incoming setpoint.
That is why I insist, the SP will change once per second (I told you 5sec, but I have managed to have an instantaneous power reading of up to 1sec) each time the power value of the inverter is updated by MQTT.
That is why I insist, the SP will change once per second
What is why you do that? The control loop can only control at a certain rate.
I don't think I'm able to explain myself with my bad English.
Let's see if I can explain it now...
Imagine that my SOLAR PANEL/Inverter is generating 1KW, I need all that power to be consumed by the water heating resistance, adjusting the dimmer dynamically.
That's why I was trying to use the power value per mqtt generated by the inverter as SP input.
That is what the flow i posted does, isn't it?
Not…. 🥹 I can't be manually adjusting the SP! The heating resistance has to be adjusted automatically to consume all the energy produced by the photovoltaic inverter.
I see now why are concerned. I thought it was obvious. The inject nodes are to simulate the solar input. Replace those with an input from the inverter.
Yes, I had actually understood that it was just for testing!
But what I want to tell you is that the value of SP will be changing every time MQTT updates the value collected from the INSTANT SOLAR POWER variable. (as you can see in the debug approximately 1 time per second)
I have modified the json, I will send it to you again to see what you think.
Apparently it seems to work, I have not yet been able to connect the DIMMER to the output, (I hope from Aliexpress).
I'm injecting the real SP into it, and it seems to respond well, although of course the PV value is still simulated.
more later....
what I want to tell you is that the value of SP will be changing every time MQTT updates the value collected from the INSTANT SOLAR POWER
As I said, that should not be a problem.
Thank you very much for your help! It would never have occurred to me to start with a kP value of 2000!!
I have to learn to tune a PID it's obvious.
Let us know how it performs when you have the hardware running.
Of course! Don't hesitate, I'll post photos of the installation so you can get an idea of the project.
As an improvement to this JSON, I would like to create some buttons to be able to select SP (remote=mqtta value) or SP Local with a manual setpoint that I can select from HomeAssistant.
And also implement a logic in which the PID does not act until the power generated by the Solar inverter does not exceed at least 300W (less than this power it is ridiculous to put it into the resistor).
Thank you again for all your time and dedication.
implement a logic in which the PID does not act until the power generated by the Solar inverter does not exceed at least 300W
You can do that by sending enable or disable to the pid node based on the power value.
Hi @colinl , I need a little help.....
I want to create a button in HA to choose a local SP (slider) and a remote SP (mqtt value).
I can't find a way to create a NODE that has 2 IN (sp local/sp remote) and 1 OUT (topic setpoint PID). And a selector to change it that would come from HA.
As you will see in the flow I am creating the entry and exit points from HA, but I don't like the idea. It would be better to create those BUTTONS inside NODERED.
I can't find a way to create a NODE that has 2 IN (sp local/sp remote) and 1 OUT (topic setpoint PID). And a selector to change it that would come from HA.
So that is three inputs, including the selector. Since those three come in different messages the first thing you have to do is to get them all into one message. To do that use a Join node. See this article in the cookbook for an example of how to join messages into one object. Have a play with the flow there and see if you can work out how to do it.
It would be better to create those BUTTONS inside NODERED.
What is the problem with doing that?