node-red-contrib-pid icon indicating copy to clipboard operation
node-red-contrib-pid copied to clipboard

PID setpoint tracking

Open isabido opened this issue 1 year ago • 160 comments

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.

image flows.json

Greetings from Spain and sorry for my terrible English.

isabido avatar Jan 05 '24 00:01 isabido

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.

colinl avatar Jan 05 '24 11:01 colinl

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!

isabido avatar Jan 05 '24 15:01 isabido

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.

image

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 image

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.

colinl avatar Jan 05 '24 17:01 colinl

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?

isabido avatar Jan 05 '24 22:01 isabido

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.

colinl avatar Jan 06 '24 10:01 colinl

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!

isabido avatar Jan 06 '24 21:01 isabido

At what rate do you get a value from the current clamp?

colinl avatar Jan 07 '24 08:01 colinl

Initially, the watt value to follow is obtained by mqtt every 5 seconds.

isabido avatar Jan 07 '24 09:01 isabido

I meant the actual power going into the resistor, that is the PV value from the current clamp.

colinl avatar Jan 07 '24 09:01 colinl

The PV value measured on the heating resistance clamp could be done at a maximum of 1 second.

isabido avatar Jan 07 '24 09:01 isabido

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.

image

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}]

colinl avatar Jan 07 '24 11:01 colinl

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? image

Thanks again for your time!

isabido avatar Jan 07 '24 20:01 isabido

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.

colinl avatar Jan 07 '24 20:01 colinl

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!

isabido avatar Jan 07 '24 21:01 isabido

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.

colinl avatar Jan 07 '24 21:01 colinl

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.

isabido avatar Jan 07 '24 21:01 isabido

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.

colinl avatar Jan 07 '24 22:01 colinl

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.

isabido avatar Jan 07 '24 22:01 isabido

That is what the flow i posted does, isn't it?

colinl avatar Jan 07 '24 22:01 colinl

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.

isabido avatar Jan 07 '24 22:01 isabido

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.

colinl avatar Jan 08 '24 07:01 colinl

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).

image flows.json

isabido avatar Jan 08 '24 11:01 isabido

I'm injecting the real SP into it, and it seems to respond well, although of course the PV value is still simulated.

image

more later....

image

isabido avatar Jan 08 '24 13:01 isabido

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.

colinl avatar Jan 08 '24 13:01 colinl

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.

isabido avatar Jan 08 '24 13:01 isabido

Let us know how it performs when you have the hardware running.

colinl avatar Jan 08 '24 13:01 colinl

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.

isabido avatar Jan 08 '24 16:01 isabido

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.

colinl avatar Jan 08 '24 17:01 colinl

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.

image image flows.json

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.

image

isabido avatar Jan 10 '24 16:01 isabido

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?

colinl avatar Jan 10 '24 21:01 colinl