Transform Javascript to function node - Help needed

Hi guys,
sorry for my bad English. I need some help to transform javascript code into a function node. The javascript is here:

// 3-Punkt-Ansteuerung aus Stellsignal
 
const Laufzeit = 153;  // Laufzeit des Stellantriebs in s
const Stellmax = 100;  // max. Stellsignal (%)
const minMove = 1;     // mindest Positionsänderung des Antriebs (in %)
const idy = 'javascript.0.scriptEnabled.common.Heizung.Stellsignal';  // Datenpunkt-ID Stellsignal
const idAuf = 'rpi2.1.gpio.8.state';  // Datenpunkt-ID Antrieb Richtung Auf
const idZu = 'rpi2.1.gpio.7.state';   // Daten

punkt-ID Antrieb Richtung Zu
 
var faktor = 1000 * Laufzeit / Stellmax;  // Faktor ms/%
var lastPos = 0;   // letzte Position (Stellsignal)
var motor = true;  // Indikator: Antrieb in Bewegung
 
// Bei Skriptstart fährt Antrieb auf Position 0 (Zu)
setState(idAuf, false); 
setState(idZu, true);
var timer = setTimeout(function() {
    setState(idZu, false);
    motor = false;
    move(getState(idy).val); // Fahren in Sollposition
}, 1000 * (Laufzeit + 10));

 
function move(pos) {
	if(!motor) {
		if(pos - lastPos > minMove) {
			setState(idAuf, true);
			motor = true;
			if(timer) clearTimeout(timer);
			timer = setTimeout(function() {
				setState(idAuf, false);
				motor = false;
			}, faktor * (pos - lastPos));
			lastPos = pos;
		}
		else if(lastPos - pos > minMove) {
			setState(idZu, true);
			motor = true;
			var deltaPos = lastPos - pos;
			if(pos === 0) deltaPos = deltaPos + 10;  // Synchronistation "Zu"
			if(timer) clearTimeout(timer);
			timer = setTimeout(function() {
				setState(idZu, false);
				motor = false;
			}, faktor * deltaPos);
			lastPos = pos;
		}	
	}
}
 
on(idy, function(dp) {
	move(dp.state.val);
});

It is a script that turns a 3 point mixer in the position its given from a PID before. Now its input ist the variable idy and it has two outputs idAuf and idZu that sets two GPIO on a RPI. The time to drive the motor is calculated by the last known position and the difference to the new position. I tried to adapt this script to a function node but I doesn't work. Now I run this script in iobroker and get the value to set the input from a flow in node red but it performs not very well. Can you help me please.

Thanks Silvio

You seem to be missing some critical parts. Where does the on object come from? And where is the setState function defined?

Remember that a function node is stand-alone. It is a node.js VM that is initialised every time a msg is sent to it. It has no external context other than the few additional things that the RED and node objects give it. (actually a few others as well such as context, global and flow).

In addition, capturing the timer var does you no good in a function node unless you retain it in a global, flow or context variable and retrieve it at the start of the code.

Hi,

I admire you writing the code... you may also want to look at nodes that specifically solve this issue such as node-red-contrib-pid.

Cheers,

Paul

Hi Paul,

I use node-red-contrib-pid to calculate the setpoint in percent. This works fine. My problem is I have a normally 3 way mixer with only a input for open and one for close. No 0-10 V signal or response. To rotate them to the calculated setpoint I need the time to switch the open or close input. A complete turn is 90° and it has a duration of 153 sec. At the beginning of the script the mixer drives 153 + 10 sec in direction close to init. Then for example the calculated setpoint is 50 % so the time to open ist 153 / 2 sec. To drive to 75 % the input to open must set on for another 153 / 4 sec. And so on. I found this script that I posted somewhere but my knowledge to adapt this to node red are not enough.

This would be a valuable addition to use with node-red-contrib-pid. It is something I have been meaning to write, but haven't got round to yet.

Edit. Does your valve have built in end stops so that it is safe to drive permanently into the end stops?

Well I have made a start. This does the initial closing of the valve, but doesn't do the actual positioning yet.

[{"id":"a06f9666.6605b8","type":"function","z":"a6823d70.1263e","name":"","func":"// msg.payload should be a required position in the range 0 to 1 \n// or a message sent regularly as a tick trigger, with topic __tick\n// or a message with topic __reset which resets it\n// the contents of the tick message are ignored\n// Output 1 is the close output, 2 is the open output\n\nconst travelTime = 5  // travel time in seconds\nconst overTravelTime = 2  // additional time to travel on startup\nconst on = 0        // values to send for motor on/off\nconst off = 1\nconst onInitialise = \"close\" // set this to \"close\", \"open\", or \"none\"\n\nif (msg.topic == \"__reset\") {\n    context.set(\"data\", null)\n    return\n}\n\nlet data = context.get(\"data\") || {initialise: true}\nconst now = new Date().getTime()\n\nif (data.initialise) {\n    if (onInitialise == \"open\"  || onInitialise == \"close\") {\n        doInitialise()\n    } else {\n        data.initialise = false\n        node.send({payload: off}, {payload: off})\n    }\n}\n// need to check initialise again as it might have finished\nif (!data.initialise) {\n    if (msg.topic !== \"__tick\") {\n        doSetRequiredPosition()\n    }\n    // always call doTick even if it a setposition, no point in waiting\n    doTick()\n}\ncontext.set(\"data\", data)\nreturn \n\nfunction doInitialise() {\n    node.warn(\"doInitialise\")\n    // have we started initialising?\n    if (data.initialiseStartTime) {\n        node.warn(\"running\")\n        // yes, have we finished\n        if (now >= data.initialiseStartTime + (travelTime + overTravelTime)*1000) {\n            node.warn(\"finished\")\n            // yes, switch off\n            node.send({payload: off}, {payload: off})\n            data.initialise = false\n        }\n        // do nothing if not finished\n    } else {\n        // haven't started initialising yet\n        node.warn(\"starting\")\n        data.initialiseStartTime = now\n        if (onInitialise == \"close\") {\n            node.send({payload: on}, null)\n        } else {\n            node.send(null, {payload: on})\n        }\n    }\n}\n\nfunction doTick() {\n    //node.warn(\"tick\")\n}\n\nfunction doSetRequiredPosition() {\n    node.warn(`set pos ${msg.payload}`)\n    data.requiredPosition = msg.payload\n}","outputs":2,"noerr":0,"initialize":"","finalize":"","x":340,"y":220,"wires":[["b84beb4e.92c988"],["60a0554a.d4abbc"]]},{"id":"4e1a8210.5e37e4","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"__tick","payload":"","payloadType":"date","x":160,"y":280,"wires":[["a06f9666.6605b8"]]},{"id":"b84beb4e.92c988","type":"debug","z":"a6823d70.1263e","name":"ONE","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":460,"y":160,"wires":[]},{"id":"60a0554a.d4abbc","type":"debug","z":"a6823d70.1263e","name":"TWO","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":470,"y":280,"wires":[]},{"id":"aab1bd22.72634","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0.1","payloadType":"num","x":100,"y":120,"wires":[["a06f9666.6605b8"]]},{"id":"3063bbb1.7ad46c","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0.5","payloadType":"num","x":110,"y":180,"wires":[["a06f9666.6605b8"]]},{"id":"832e0919.c1b8b8","type":"inject","z":"a6823d70.1263e","name":"Reset","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"__reset","payload":"","payloadType":"date","x":90,"y":240,"wires":[["a06f9666.6605b8"]]}]

Hi Colin,

yes it stops at the end. It has 2 switches to turn power off.

Give this a try. The Valve Motor Drive function is the bit that does the work. Setup the constants at the top to match your motor. It expects a value in msg.payload between 0 and 1 which matches the pid node.
If the topic is set to __reset then that forces the drive to the end stop.
The inject nodes are for injecting test values to see if it works.
You can inject new values at any time, even if it is currently moving, and it will immediately start moving to that position unless it is still doing the initial drive in which case it will complete that then go to whatever input was last requested.

The bit at the bottom is a motor simulator that draws a chart showing where the motor currently is. The constants in there need to be set to match those in the VMD function.

Test it first just with the inject nodes and the chart to see if it looks ok, then if that is ok connect up the motor, but leave the chart on too, and see if that is ok. Then you can try connecting it to the pid node, if you are at that stage in the build.

Any time it is told to go to position 0 or 1 it will add on the overtravel time to help it to stay in sync. If a non-endstop value is given while that is happening then it will start the move to the new position, unless it is during a Reset operation as described above.

If you set the Reset inject to fire on startup then it will initialise when node-red is started.

The third output on the function node outputs diagnostics which may be helpful if there are problems.

[{"id":"a06f9666.6605b8","type":"function","z":"a6823d70.1263e","name":"Valve Motor Drive","func":"// msg.payload should be a required position in the range 0 to 1\n// or a message with topic \"__reset\" which resets it\n// Output 1 is the close output, 2 is the open output\n// output 3 has diagnostic messages to help with debug\n\n// set these up to match your motor\nconst travelTime = 10000  // travel time in milliseconds\nconst overTravelTime = 2000  // additional time to travel to make sure end stop is reached, milliseconds\nconst minMove = 0.01    // the minimum distance to travel\nconst on = 0        // values to send for motor on/off\nconst off = 1\nconst onInitialise = \"close\" // set this to \"close\", \"open\", or \"none\"\n// if onInitialise is set to \"none\" then set this to initial position to be assumed. Range 0 to 1\n// otherwise it doesn't matter \nconst initialPosition = 0\n\n// reset context if topic is __reset\nif (msg.topic == \"__reset\") {\n    let data = context.get(\"data\") || {}\n    diags(\"reset\")\n    if (data.timer) {\n        clearTimeout(data.timer)\n        data.timer = 0\n    }\n    drive(0)\n    context.set(\"data\", null)\n    // setup required position after initialise\n    if (onInitialise === \"close\") {\n        msg.payload = 0\n    } else if (onInitialise === \"open\") {\n        msg.payload = 1\n    } else {\n        // no movement required on initialise so set it to the assumed position\n        msg.payload = initialPosition\n    }\n} \n\nlet data = context.get(\"data\") || {initialise: true}\n\nconst now = new Date().getTime()\n\nif (data.initialise) {\n    if (onInitialise == \"open\"  || onInitialise == \"close\") {\n        doInitialise()\n    } else {\n        // drive to end stop not required on startup\n        data.initialise = false\n        data.currentPosition = initialPosition\n        drive(0)\n    }\n}\n\n// msg.payload contains required position\ndoSetRequiredPosition(msg.payload)\n\ncontext.set(\"data\", data)\nreturn \n\nfunction doInitialise() {\n    diags(\"doInitialise\")\n    // have we started initialising?\n    if (data.timer) {\n        // yes, so do nothing\n        diags(\"initialise running\")\n    } else {\n        // haven't started initialising yet\n        diags(\"starting initialise\")\n        let pos     // required position\n        // setup required position and pretend currently at the other end, to force full move\n        if (onInitialise == \"close\") {\n            pos = 0\n            data.currentPosition = 1\n        } else {  // must be open\n            pos = 1\n            data.currentPosition = 0\n        }\n        doMove(pos)  // start the move\n    }\n}\n\nfunction doSetRequiredPosition(pos) {\n    // required position is in msg.payload\n    diags(`set pos ${pos}`)\n    data.requiredPosition = pos\n    if (!data.initialise) {\n        // start it moving\n        doMove(data.requiredPosition)\n    }\n}\n\nfunction doMove(position) {\n    // initiates a drive from current position to passed in position\n    // update the current position in case already moving\n    updatePosition()\n    diags(`current: ${data.currentPosition},  required: ${position}`)\n    const driveDistance = position - data.currentPosition\n    let driveTime = Math.abs( driveDistance * travelTime ) // msec\n    const direction = Math.sign( driveDistance )\n    // if moving to end stop then add on the extra to ensure gets there\n    if (position === 0  ||  position === 1) {\n        diags(\"adding overtravel\")\n        driveTime += overTravelTime\n    }\n    diags(`distance: ${driveDistance}, time: ${driveTime}, dir: ${direction}`)\n    // clear timer for any existing motion\n    if (data.timer) {\n        diags(\"Already moving\")\n        clearTimeout(data.timer)\n        data.timer = 0\n    }\n    // start move, or ensure stopped if already there, or close enough\n    if (Math.abs(driveDistance) > minMove) {\n        // start it and stop after timeout\n        drive(direction)\n        data.travelStartTime = new Date().getTime()\n        data.travelDirection = direction\n        data.timer = setTimeout( function() {\n            // diags(\"timeout\")\n            drive(0)\n            // fetch data again in case it has changed\n            data = context.get(\"data\")\n            // update the current position\n            updatePosition()\n            data.timer = 0\n            if (data.initialise) {\n                // finished initialising, clear flag\n                data.initialise = false\n                // and start move to required position if one has been given\n                if (data.requiredPosition) {\n                    doSetRequiredPosition(data.requiredPosition)\n                }\n            }\n            context.set(\"data\", data)\n        }, driveTime)\n    } else {\n        drive(0)\n        data.initialise = false\n    }\n}\n\nfunction updatePosition() {\n    // updates the current position if necessary\n    if (data.timer) {\n        const now = new Date().getTime()\n        const timeTravelled = now - data.travelStartTime\n        data.currentPosition += timeTravelled/travelTime * data.travelDirection\n        // update travel start time in case called again\n        data.travelStartTime = now\n        // clamp to 0:1\n        data.currentPosition = Math.min(Math.max(data.currentPosition, 0), 1);\n        diags(`updated position: ${data.currentPosition}`)\n        node.status(`${data.currentPosition}`)\n    }\n}\n\n// given direction to drive, or 0 for stop\nfunction drive( direction ) {\n    let op1 = off\n    let op2 = off\n    if (direction > 0) {\n        diags(\"open\")\n        op2 = on\n        node.status(\"open\")\n    } else if (direction < 0) {\n        diags(\"close\")\n        op1 = on\n        node.status(\"close\")\n    } else {\n        diags(\"stop\")\n        node.status(\"stop\")\n    }\n    node.send([{payload: op1}, {payload: op2}])\n}\n\nfunction diags(str) {\n    node.send([null, null, {payload: str}])\n}","outputs":3,"noerr":0,"initialize":"","finalize":"","x":370,"y":220,"wires":[["b84beb4e.92c988","735f7613.f180e8"],["60a0554a.d4abbc","5c005eb3.30a5b"],["41373436.efd2f4"]]},{"id":"b84beb4e.92c988","type":"debug","z":"a6823d70.1263e","name":"CLOSE","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":540,"y":140,"wires":[]},{"id":"60a0554a.d4abbc","type":"debug","z":"a6823d70.1263e","name":"OPEN","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":530,"y":320,"wires":[]},{"id":"aab1bd22.72634","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0.1","payloadType":"num","x":90,"y":120,"wires":[["a06f9666.6605b8"]]},{"id":"3063bbb1.7ad46c","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0.5","payloadType":"num","x":90,"y":160,"wires":[["a06f9666.6605b8"]]},{"id":"832e0919.c1b8b8","type":"inject","z":"a6823d70.1263e","name":"Reset","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"__reset","payload":"","payloadType":"date","x":90,"y":340,"wires":[["a06f9666.6605b8"]]},{"id":"41373436.efd2f4","type":"debug","z":"a6823d70.1263e","name":"DIAGS","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":350,"y":340,"wires":[]},{"id":"85c32710.7cf33","type":"function","z":"a6823d70.1263e","name":"Motor simulator","func":"// if topic is open or close then payload contains combined values from VMD \n// otherwise this is a trigger to update the output\n// set these up to match your motor\nconst travelTime = 10000  // travel time in milliseconds\nconst on = 0        // values to send for motor on/off\nconst off = 1\nconst startPos = 0.5     // assumed start pos\n\nlet data = context.get(\"data\") || {pos: startPos, lastTime: new Date().getTime(), dir: 0}\n\nupdatePosition()\nif (msg.topic === \"open\"  || msg.topic === \"close\") {\n    if (msg.payload.open == off  &&  msg.payload.close == off ) {\n        data.dir = 0\n    } else if (msg.payload.open == on) {\n        data.dir = 1\n    } else {\n        data.dir = -1\n    }\n}\n\nmsg.payload = data.pos\ncontext.set(\"data\", data)\nreturn msg;\n\nfunction updatePosition() {\n    const now = new Date().getTime()\n    deltaT = now - data.lastTime\n    data.pos += deltaT/travelTime*data.dir\n    data.pos = Math.min(Math.max(data.pos, 0), 1);\n    data.lastTime = now\n}","outputs":1,"noerr":0,"initialize":"","finalize":"","x":480,"y":500,"wires":[["40d90aa8.f0ea14"]]},{"id":"94f6aa43.1dd0c","type":"join","z":"a6823d70.1263e","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":310,"y":500,"wires":[["85c32710.7cf33"]]},{"id":"735f7613.f180e8","type":"link out","z":"a6823d70.1263e","name":"","links":["ae12793f.b5aa38"],"x":535,"y":200,"wires":[]},{"id":"5c005eb3.30a5b","type":"link out","z":"a6823d70.1263e","name":"","links":["dff075f6.329a7"],"x":535,"y":240,"wires":[]},{"id":"dff075f6.329a7","type":"link in","z":"a6823d70.1263e","name":"","links":["5c005eb3.30a5b"],"x":75,"y":520,"wires":[["6bdc8f0.9652df"]]},{"id":"ae12793f.b5aa38","type":"link in","z":"a6823d70.1263e","name":"","links":["735f7613.f180e8"],"x":75,"y":480,"wires":[["8430a6a.725de58"]]},{"id":"6bdc8f0.9652df","type":"change","z":"a6823d70.1263e","name":"open","rules":[{"t":"set","p":"topic","pt":"msg","to":"open","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":170,"y":520,"wires":[["94f6aa43.1dd0c"]]},{"id":"8430a6a.725de58","type":"change","z":"a6823d70.1263e","name":"close","rules":[{"t":"set","p":"topic","pt":"msg","to":"close","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":170,"y":480,"wires":[["94f6aa43.1dd0c"]]},{"id":"ad4ded39.57dbb8","type":"inject","z":"a6823d70.1263e","name":"trigger","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":340,"y":540,"wires":[["85c32710.7cf33"]]},{"id":"fe308e8b.19a588","type":"ui_chart","z":"a6823d70.1263e","name":"","group":"61285987.c20328","order":4,"width":0,"height":0,"label":"chart","chartType":"line","legend":"false","xformat":"HH:mm:ss","interpolate":"linear","nodata":"","dot":false,"ymin":"0","ymax":"1","removeOlder":1,"removeOlderPoints":"","removeOlderUnit":"3600","cutout":0,"useOneColor":false,"useUTC":false,"colors":["#1f77b4","#aec7e8","#ff7f0e","#2ca02c","#98df8a","#d62728","#ff9896","#9467bd","#c5b0d5"],"outputs":1,"useDifferentColor":false,"x":710,"y":580,"wires":[[]]},{"id":"40d90aa8.f0ea14","type":"change","z":"a6823d70.1263e","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"Position","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":510,"y":580,"wires":[["fe308e8b.19a588"]]},{"id":"53418660.8ec89","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"1","payloadType":"num","x":90,"y":280,"wires":[["a06f9666.6605b8"]]},{"id":"d7bebc9d.9eae5","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0","payloadType":"num","x":90,"y":80,"wires":[["a06f9666.6605b8"]]},{"id":"ab1c19b4.2a346","type":"inject","z":"a6823d70.1263e","name":"Clear","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[]","payloadType":"json","x":550,"y":620,"wires":[["fe308e8b.19a588"]]},{"id":"f61d461a.77501","type":"comment","z":"a6823d70.1263e","name":"Motor Simulator","info":"","x":300,"y":420,"wires":[]},{"id":"dd261e02.b6ea18","type":"comment","z":"a6823d70.1263e","name":"Close output","info":"","x":650,"y":200,"wires":[]},{"id":"b01803b4.af96b8","type":"comment","z":"a6823d70.1263e","name":"Open output","info":"","x":650,"y":240,"wires":[]},{"id":"9a4798a2.d18198","type":"comment","z":"a6823d70.1263e","name":"Set constants here to match VMD function","info":"","x":570,"y":460,"wires":[]},{"id":"82f11909.396448","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0.508","payloadType":"num","x":90,"y":200,"wires":[["a06f9666.6605b8"]]},{"id":"37c15095.10c73","type":"inject","z":"a6823d70.1263e","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"0.512","payloadType":"num","x":90,"y":240,"wires":[["a06f9666.6605b8"]]},{"id":"61285987.c20328","type":"ui_group","name":"Main","tab":"e7c46d5e.a1283","order":1,"disp":true,"width":"6","collapse":false},{"id":"e7c46d5e.a1283","type":"ui_tab","name":"Dashboard","icon":"dashboard"}]

@SilSchmie have you been able to try this?

Hello Colin,
sorry for replying so late. I only noticed yesterday that there was something new in the thread because the notification email ended up in spam. I've been testing since yesterday. I am very impressed. It works immediately as it should! I did not test the motor simulator, instead I let the function node run parallel to my old script and observed the behavior. It works perfectly. Then I connected the mixer. It regulates the flow temperature really well. I have been using this flow in my heating system since yesterday. It was really cold that night (-23 ° C) and we didn't freeze :slight_smile:
It works much better now than before, because there was always a delay when a flow calculated the control signal and the Javascript controlled the motor with the control signal. This is now happening without delay. Before that, I could never find the right parameters for the controller because it began to oscillate or overshoot again and again. There are some disruptive factors in this control loop that make correct controller parameterization difficult, but I think it will now be easier :slight_smile:
Colin, what can I say. I am so happy for your help. I would never have made it. Thanks alot!

[{"id":"a9a7ae85.6bf5d","type":"tab","label":"PID Vorlauf","disabled":false,"info":""},{"id":"9158dc45.b6763","type":"ioBroker in","z":"a9a7ae85.6bf5d","name":"Soll_Vorlauf","topic":"javascript.0.scriptEnabled.common.Heizung.Soll_Vorlauf","payloadType":"value","onlyack":"","func":"all","gap":"","fireOnStart":"false","x":270,"y":100,"wires":[["1d8df79e.14a708"]]},{"id":"51cd26c7.8d53d8","type":"PID","z":"a9a7ae85.6bf5d","name":"","setpoint":"0","pb":"44","ti":"105","td":"0","integral_default":"0","smooth_factor":"0","max_interval":"600","enable":"1","disabled_op":"0","x":570,"y":260,"wires":[["1d5dec7e.858db4","ecb09c7e.a8067"]]},{"id":"28c2776.d88ff88","type":"ioBroker in","z":"a9a7ae85.6bf5d","name":"Vorlauf","topic":"owfs.0.wires.Vorlauf","payloadType":"value","onlyack":"","func":"all","gap":"","fireOnStart":"false","x":250,"y":160,"wires":[["51cd26c7.8d53d8"]]},{"id":"1d8df79e.14a708","type":"change","z":"a9a7ae85.6bf5d","name":"","rules":[{"t":"set","p":"setpoint","pt":"msg","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":100,"wires":[["51cd26c7.8d53d8"]]},{"id":"1d5dec7e.858db4","type":"range","z":"a9a7ae85.6bf5d","minin":"0","maxin":"1","minout":"0","maxout":"100","action":"scale","round":false,"property":"payload","name":"","x":700,"y":220,"wires":[["402dbf5d.ce86b"]]},{"id":"402dbf5d.ce86b","type":"ioBroker out","z":"a9a7ae85.6bf5d","name":"Stellsignal","topic":"javascript.0.scriptEnabled.common.Heizung.Stellsignal","ack":"true","autoCreate":"false","stateName":"","role":"","payloadType":"","readonly":"","stateUnit":"","stateMin":"","stateMax":"","x":890,"y":220,"wires":[]},{"id":"ecb09c7e.a8067","type":"function","z":"a9a7ae85.6bf5d","name":"Valve Motor Drive","func":"// msg.payload should be a required position in the range 0 to 1\n// or a message with topic \"__reset\" which resets it\n// Output 1 is the close output, 2 is the open output\n// output 3 has diagnostic messages to help with debug\n\n// set these up to match your motor\nconst travelTime = 153000  // travel time in milliseconds\nconst overTravelTime = 2000  // additional time to travel to make sure end stop is reached, milliseconds\nconst minMove = 0.005    // the minimum distance to travel\nconst on = 1        // values to send for motor on/off\nconst off = 0\nconst onInitialise = \"close\" // set this to \"close\", \"open\", or \"none\"\n// if onInitialise is set to \"none\" then set this to initial position to be assumed. Range 0 to 1\n// otherwise it doesn't matter \nconst initialPosition = 0\n\n// reset context if topic is __reset\nif (msg.topic == \"__reset\") {\n    let data = context.get(\"data\") || {}\n    diags(\"reset\")\n    if (data.timer) {\n        clearTimeout(data.timer)\n        data.timer = 0\n    }\n    drive(0)\n    context.set(\"data\", null)\n    // setup required position after initialise\n    if (onInitialise === \"close\") {\n        msg.payload = 0\n    } else if (onInitialise === \"open\") {\n        msg.payload = 1\n    } else {\n        // no movement required on initialise so set it to the assumed position\n        msg.payload = initialPosition\n    }\n} \n\nlet data = context.get(\"data\") || {initialise: true}\n\nconst now = new Date().getTime()\n\nif (data.initialise) {\n    if (onInitialise == \"open\"  || onInitialise == \"close\") {\n        doInitialise()\n    } else {\n        // drive to end stop not required on startup\n        data.initialise = false\n        data.currentPosition = initialPosition\n        drive(0)\n    }\n}\n\n// msg.payload contains required position\ndoSetRequiredPosition(msg.payload)\n\ncontext.set(\"data\", data)\nreturn \n\nfunction doInitialise() {\n    diags(\"doInitialise\")\n    // have we started initialising?\n    if (data.timer) {\n        // yes, so do nothing\n        diags(\"initialise running\")\n    } else {\n        // haven't started initialising yet\n        diags(\"starting initialise\")\n        let pos     // required position\n        // setup required position and pretend currently at the other end, to force full move\n        if (onInitialise == \"close\") {\n            pos = 0\n            data.currentPosition = 1\n        } else {  // must be open\n            pos = 1\n            data.currentPosition = 0\n        }\n        doMove(pos)  // start the move\n    }\n}\n\nfunction doSetRequiredPosition(pos) {\n    // required position is in msg.payload\n    diags(`set pos ${pos}`)\n    data.requiredPosition = pos\n    if (!data.initialise) {\n        // start it moving\n        doMove(data.requiredPosition)\n    }\n}\n\nfunction doMove(position) {\n    // initiates a drive from current position to passed in position\n    // update the current position in case already moving\n    updatePosition()\n    diags(`current: ${data.currentPosition},  required: ${position}`)\n    const driveDistance = position - data.currentPosition\n    let driveTime = Math.abs( driveDistance * travelTime ) // msec\n    const direction = Math.sign( driveDistance )\n    // if moving to end stop then add on the extra to ensure gets there\n    if (position === 0  ||  position === 1) {\n        diags(\"adding overtravel\")\n        driveTime += overTravelTime\n    }\n    diags(`distance: ${driveDistance}, time: ${driveTime}, dir: ${direction}`)\n    // clear timer for any existing motion\n    if (data.timer) {\n        diags(\"Already moving\")\n        clearTimeout(data.timer)\n        data.timer = 0\n    }\n    // start move, or ensure stopped if already there, or close enough\n    if (Math.abs(driveDistance) > minMove) {\n        // start it and stop after timeout\n        drive(direction)\n        data.travelStartTime = new Date().getTime()\n        data.travelDirection = direction\n        data.timer = setTimeout( function() {\n            // diags(\"timeout\")\n            drive(0)\n            // fetch data again in case it has changed\n            data = context.get(\"data\")\n            // update the current position\n            updatePosition()\n            data.timer = 0\n            if (data.initialise) {\n                // finished initialising, clear flag\n                data.initialise = false\n                // and start move to required position if one has been given\n                if (data.requiredPosition) {\n                    doSetRequiredPosition(data.requiredPosition)\n                }\n            }\n            context.set(\"data\", data)\n        }, driveTime)\n    } else {\n        drive(0)\n        data.initialise = false\n    }\n}\n\nfunction updatePosition() {\n    // updates the current position if necessary\n    if (data.timer) {\n        const now = new Date().getTime()\n        const timeTravelled = now - data.travelStartTime\n        data.currentPosition += timeTravelled/travelTime * data.travelDirection\n        // update travel start time in case called again\n        data.travelStartTime = now\n        // clamp to 0:1\n        data.currentPosition = Math.min(Math.max(data.currentPosition, 0), 1);\n        diags(`updated position: ${data.currentPosition}`)\n        node.status(`${data.currentPosition}`)\n    }\n}\n\n// given direction to drive, or 0 for stop\nfunction drive( direction ) {\n    let op1 = off\n    let op2 = off\n    if (direction > 0) {\n        diags(\"open\")\n        op2 = on\n        node.status(\"open\")\n    } else if (direction < 0) {\n        diags(\"close\")\n        op1 = on\n        node.status(\"close\")\n    } else {\n        diags(\"stop\")\n        node.status(\"stop\")\n    }\n    node.send([{payload: op1}, {payload: op2}])\n}\n\nfunction diags(str) {\n    node.send([null, null, {payload: str}])\n}","outputs":3,"noerr":0,"initialize":"","finalize":"","x":570,"y":500,"wires":[["f2ccb9e3.84ab78","fba4dec6.e8f15","44ae8874.728a18"],["ee523de0.6ddcb","c761b826.2efc38","b7fff5f7.56b078"],["2e3e22ed.db522e"]]},{"id":"f2ccb9e3.84ab78","type":"debug","z":"a9a7ae85.6bf5d","name":"CLOSE","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":880,"y":440,"wires":[]},{"id":"ee523de0.6ddcb","type":"debug","z":"a9a7ae85.6bf5d","name":"OPEN","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":870,"y":500,"wires":[]},{"id":"b6e8fb55.35f948","type":"inject","z":"a9a7ae85.6bf5d","name":"Reset","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"__reset","payload":"","payloadType":"date","x":330,"y":500,"wires":[["ecb09c7e.a8067"]]},{"id":"2e3e22ed.db522e","type":"debug","z":"a9a7ae85.6bf5d","name":"DIAGS","active":false,"tosidebar":true,"console":false,"tostatus":true,"complete":"payload","targetType":"msg","statusVal":"payload","statusType":"auto","x":880,"y":620,"wires":[]},{"id":"fba4dec6.e8f15","type":"link out","z":"a9a7ae85.6bf5d","name":"","links":["ae12793f.b5aa38"],"x":715,"y":380,"wires":[]},{"id":"c761b826.2efc38","type":"link out","z":"a9a7ae85.6bf5d","name":"","links":["dff075f6.329a7"],"x":695,"y":600,"wires":[]},{"id":"1940064b.f1bdea","type":"comment","z":"a9a7ae85.6bf5d","name":"Close output","info":"","x":710,"y":340,"wires":[]},{"id":"d525fa7.d9cb508","type":"comment","z":"a9a7ae85.6bf5d","name":"Open output","info":"","x":710,"y":640,"wires":[]},{"id":"44ae8874.728a18","type":"ioBroker out","z":"a9a7ae85.6bf5d","name":"Mischer ZU","topic":"rpi2.1.gpio.7.state","ack":"false","autoCreate":"false","stateName":"","role":"","payloadType":"","readonly":"","stateUnit":"","stateMin":"","stateMax":"","x":890,"y":380,"wires":[]},{"id":"b7fff5f7.56b078","type":"ioBroker out","z":"a9a7ae85.6bf5d","name":"Mischer AUF","topic":"rpi2.1.gpio.8.state","ack":"false","autoCreate":"false","stateName":"","role":"","payloadType":"","readonly":"","stateUnit":"","stateMin":"","stateMax":"","x":890,"y":560,"wires":[]},{"id":"9e46f204.78c24","type":"comment","z":"a9a7ae85.6bf5d","name":"Visualisierung","info":"","x":920,"y":180,"wires":[]}]

That's great to know. I think that must be about the fourth time I have implemented the algorithm using various technologies over the last forty years. The first time was on an 8 bit microcontroller.
One issue could be that you find the position drifts with time relative to the position the algorithm thinks the motor is at. Suppose for example that the motor actually drives slightly faster in one direction than the other, then each time the motor opens a bit and then closes the same amount it will not end up in quite the right position. That could be improved by providing different travel times for the two directions. Another issue could be that on short movements it probably over or undershoots slightly, so if it moved from 1/4 open to 3/4 open in ten small steps and then back again in 1 step then again it might not end up back at exactly the right position. You won't notice this in terms of the process response because the integral will adjust automatically to compensate, but eventually you might get to the state where the pid node wants to close the valve a bit but the node output is only just above zero even thought the valve is actually half open. What will happen then is that the algorithm will see the fact that it has been asked to go to zero so it will overdrive in that direction to make sure it is on the end stop, so the valve will then close or at least move more than required in that direction, which could give you a kick on the process. The same could happen in the other direction.

Apart from making sure the algorithm matches the valve as closely as possible there isn't much that can be done about that.

Perhaps it would be helpful to create an approval input in this regard. This could be used, for example, at night to close the mixer and have it regulated again in the morning. So a Initialization takes place every day, even if the mixer never has to go to 0 or 1 in normal operation.

There is already a Reset input as in the test flow, which re-initialises it. That is enough isn't it?

So I would have to have a message with the topic "__reset" at the input of the Valve Motor Drive function for e.g. 10 p.m. to 6 a.m. in order to keep the mixer closed overnight?

I would use a node-red-contrib-simple-gate to block messages from the sensor getting to the motor drive node between those times. Send it a block command at 10pm and open command at 6am, then at just after 10 send the motor drive function a reset.

Also, after the Reset send zero so that it does the park. It doesn't do that automatically on reset, it does it when the first position command is received. That is because you might want to reset to fully open, not fully closed. In fact it would be good if the reset interpreted the payload as the position to reset to. If you would like to work out a fix for that it would be good.
I plan to package it up into a subflow before publishing it on the flows site. Then the settings will be in subflow config so no need to hack the values in the function node.
Not sure when I will have time to do that though.

Hello Colin,
Thanks for your great help. I also just noticed another way of doing it easily. Via the eneable input of the PID controller. There the input changes from eneable to disable at 10 p.m. and the controller also closes the motor if disable = 0.

Yes, of course, I should have thought of that. That is all you need do. :slight_smile: