A time proportion node for driving on/off devices in a pseudo linear way

This flow shows a function node that converts a floating value in the range 0.0 to 1.0 into a time proportioned on/off signal. It can be used, for example, drive a heater that only has on/off control such as to effectively get any power between 0 and 100%. Similarly it could be used to control a valve on a radiator to control the heat output.

It can be used in conjunction with a closed loop control node such as node-red-contrib-pid to control (again, for example) a heating system.

The comments at the start of the function node should be sufficient to configure it to individual circumstances.

[
    {
        "id": "b51fa4f0.7b618",
        "type": "mqtt in",
        "z": "fd0a1625.33a968",
        "name": "",
        "topic": "tydwr/conservatory/power",
        "qos": "1",
        "broker": "2d6ff793.95b218",
        "x": 262,
        "y": 686,
        "wires": [
            [
                "bf6ecf4b.1348d"
            ]
        ]
    },
    {
        "id": "bf6ecf4b.1348d",
        "type": "function",
        "z": "fd0a1625.33a968",
        "name": "Timeprop",
        "func": "// A node that can be used to generate a time proportioned on/off signal\n// from a power requirement value in the range 0 to 1\n// So for example with a cycle time period (set below) of 10 minutes and\n// a power requirement of 0.2 the output will be on for 2 minutes in every\n// ten minutes.\n// In addition to passing in messages with the payload set to the current\n// power requirement (floating point 0.0 to 1.0), provide an input from a \n// repeating inject node with the topic set to 'tick' and the payload \n// containing the current timestamp.  The frequency of this will depend upon \n// the cycle time required. For a cycle time period of 10 minutes I use an inject\n// repeat of 5 seconds.\n\n// Set these three variables as required\nvar period = 10*60*1000;  // On/off cycle time period millisecs, 10 minutes\nvar deadTime = 15*1000;   // number of milliseconds the valve (or whatever) takes to actuate, 30 seconds\nvar invert = true;        // set true for active low output, so the output will go low\n                          // when the valve should actuate, this is the usual case on a\n                          // pi feeding a relay to drive the actuator\n\n// is this a tick message?\nif (msg.topic !== \"tick\") {\n    // no, so it should be a power value, save it and exit\n    var power = msg.payload;\n    context.set('power', msg.payload);\n    msg = null;\n} else {\n    // yes, payload is timestamp, calc current wave value between 0 and 1\n    var wave = (msg.payload % period)/period;     // fraction of way through cycle\n    var direction;\n    // determine direction of travel and convert to triangular wave\n    if (wave < 0.5) {\n        direction = 1;      // on the way up\n        wave = wave*2;\n    } else {\n        direction = -1;     // on the way down\n        wave = (1 - wave)*2;\n    }\n    var requestedPower = context.get('power') || 0;\n    // if a dead_time has been supplied for this o/p then adjust power accordingly\n    if (deadTime > 0  && requestedPower > 0.0  &&  requestedPower < 1.0) {\n        var dtop = deadTime/period;\n        power = (1.0-2.0*dtop)*requestedPower + dtop;\n    } else {\n        power = requestedPower;\n    }\n    //  cope with end cases in case values outside 0..1\n    var opState;\n    if (power <= 0.0) {\n        opState = 0;     // no heat\n    } else if (power >= 1.0) {\n        opState = 1;     // full heat\n    } else {\n        // only allow power to come on on the way down and off on the way up, to reduce short pulses\n        if (power >= wave  &&  direction === -1) {\n            opState = 1;\n        } else if (power <= wave  &&  direction === 1) {\n            opState = 0;\n        } else {\n            // otherwise leave it as it is\n            opState = context.get('opState') || 0;\n        }      \n    }\n    context.set('opState', opState);\n    msg.payload = invert  ?  (1-opState)  :  opState;\n}\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 547.5,
        "y": 685,
        "wires": [
            [
                "786e2f70.979ef"
            ]
        ]
    },
    {
        "id": "95370fda.bc8ea",
        "type": "inject",
        "z": "fd0a1625.33a968",
        "name": "1 sec",
        "topic": "tick",
        "payload": "",
        "payloadType": "date",
        "repeat": "1",
        "crontab": "",
        "once": true,
        "x": 481.5,
        "y": 623,
        "wires": [
            [
                "bf6ecf4b.1348d"
            ]
        ]
    },
    {
        "id": "786e2f70.979ef",
        "type": "rpi-gpio out",
        "z": "fd0a1625.33a968",
        "name": "",
        "pin": "13",
        "set": true,
        "level": "1",
        "out": "out",
        "x": 765,
        "y": 684,
        "wires": []
    },
    {
        "id": "2d6ff793.95b218",
        "type": "mqtt-broker",
        "z": "fd0a1625.33a968",
        "broker": "localhost",
        "port": "1883",
        "clientid": "",
        "usetls": false,
        "compatmode": true,
        "keepalive": "60",
        "cleansession": true,
        "willTopic": "",
        "willQos": "0",
        "willPayload": "",
        "birthTopic": "",
        "birthQos": "0",
        "birthPayload": ""
    }
]
colinl

Flow Info

created 6 months, 3 weeks ago

Node Types

Core
  • function (x1)
  • inject (x1)
  • mqtt in (x1)
  • mqtt-broker (x1)
  • rpi-gpio out (x1)

Tags

  • node
  • pid
  • control
  • proportion
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option