Simplify flow using cronplus

Had an offtopic discussion with @Steve-Mcl elsewhere so making a dedicated discussion here inestad.

I have enjoyed the cronplus node as it is ridiculously good, so flexible, supports timezones and everything! My only minor gripe was that it requires topic (which I simply set to space (' '). I get that topic is useful when it's useful, but it is useless when not. But it's just a dead prop, so ok. Ideally it would be able to set it as missing value, or rather not existent at all.

I got one typical use case for me where it came short of one thing. I need to read X amount of props from a device. With all the bells and whistles that comes with it. Send array of props to read on one of those finicky protocols, with parts so they can be grouped together afterwards, which mean I can't discard failed reads. However, in this particular case, I need to read different variables at different times. Let's say prop a, b, c every minute, then d and e every hour and so forth.

Example:

[
    {
        "id": "da53f33028604d74",
        "type": "cronplus",
        "z": "77897c4305a6f68a",
        "name": "minute",
        "outputField": "payload",
        "timeZone": "",
        "storeName": "",
        "commandResponseMsgOutput": "output1",
        "defaultLocation": "",
        "defaultLocationType": "default",
        "outputs": 1,
        "options": [
            {
                "name": "schedule1",
                "topic": " ",
                "payloadType": "str",
                "payload": "",
                "expressionType": "cron",
                "expression": "* * * * *",
                "location": "",
                "offset": "0",
                "solarType": "all",
                "solarEvents": "sunrise,sunset"
            }
        ],
        "x": 110,
        "y": 320,
        "wires": [
            [
                "7c4b052d5ea05174"
            ]
        ]
    },
    {
        "id": "7c4b052d5ea05174",
        "type": "function",
        "z": "77897c4305a6f68a",
        "name": "prepare read",
        "func": "node.send([null, { reset: true }]);\nconst requests = {\n  minute: [\n    { topic: \"power_production\",    value: 0, fc: 4, address: 3004, quantity:  2 }, // FC4 - 3005-06\n    { topic: \"fault_code\",          value: 0, fc: 4, address: 3095, quantity:  5 }, // FC4 - 3096-3100\n    { topic: \"grid_frequency\",      value: 0, fc: 4, address: 3042, quantity:  1 }, // FC4 - 3043\n    { topic: \"cabinet_temperature\", value: 0, fc: 4, address: 3041, quantity:  1 }, // FC4 - 3042 (inverter temp)\n    { topic: \"dc_voltage_current\",  value: 0, fc: 4, address: 3021, quantity:  8 }, // FC4 - 3022-29\n    { topic: \"status_text\", value: 0, fc: 4, address: 3029, quantity: 16 }, // FC4 - 3030-45 (alarm code and inverter status)\n  ],\n  hour: [\n    { topic: \"energy_production\",   value: 0, fc: 4, address: 3008, quantity:  2 }, // FC4 - 3009-10\n  ],\n  month: [\n    { topic: \"energy_last_month\",   value: 0, fc: 4, address: 3012, quantity:  2 }, // FC4 - 3013-14\n  ],\n  year: [\n    { topic: \"energy_last_year\",    value: 0, fc: 4, address: 3018, quantity:  2 } // FC4 - 3019-20\n  ]\n};\n\nlet messages = requests.minute;\n\nconst now = new Date();\nnow.setSeconds(0, 0);\nif (now.getMinutes() === 0) {\n  messages = [...messages, ...requests.hour]; // add hour\n}\nconst isMonth = now.getHours() === 0 && now.getMinutes() === 10 && now.getDate() === 1; // 00:10 on day 1 of each month\nif (isMonth) {\n  messages = [...messages, ...requests.month]; // add month\n}\n\nconst isYear = now.getHours() === 0 && now.getMinutes() === 30 && now.getDate() === 1 && (now.getMonth() + 1) === 1; // 00:30 on day 1 of january\nif (isYear) {\n  messages = [...messages, ...requests.year]; // add year\n}\n\nconst id = RED.util.generateId();\n\nmessages.forEach((message, i) => {\n  message.ts = now.getTime();\n  // set request in payload\n  message.payload = {\n    value: message.value,\n    fc: message.fc,\n    address: message.address,\n    quantity: message.quantity\n  };\n  // delete junk\n  delete message.value;\n  delete message.fc;\n  delete message.address;\n  delete message.quantity;\n\n  // join\n  message.parts = {\n    id: id,\n    index: i,\n    count: messages.length,\n    type: \"array\",\n    len: 1\n  };\n});\n\nreturn [messages, null];",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 250,
        "y": 320,
        "wires": [
            [
                "8529df046f3b90fb"
            ],
            [
                "071f6a594e907588"
            ]
        ],
        "outputLabels": [
            "Messages",
            "Reset join"
        ]
    }
]

This is the bigger picture (subflow):

Unfortunately I had to revert the crontab syntax in favour of javascript logic to be able to get a single message at every schedule! Any way to get just one message regardless? Sort of like switch can send on first match or every match.

Can you explain a bit more what the overall flow is doing ?

Just looking at it - looks like you are doing a modbus get - presumably for multiple values - which you then seem to split out with the switch node - then do something with the Buffer plus nodes - then join them all back togerher ?

Is there a reason you can not use the Buffer node to output multiple values into a key pair sequence ?

So a bit of a high level overview please

Craig

Why? That doesnt make sense? You could have chose fan-out mode or used a switch node after the cron to route the messages based on topic (see how its useful :wink: )



Anyhow, this would possibly be my approach:

Features:

  1. all registers required are gotten in one network comm - the parser picks out the values of use
  2. The link-call subroutine guarantees comms are sent to the network serially
[{"id":"da53f33028604d74","type":"cronplus","z":"2592e3385ebb8175","name":"minute \\n hour \\n month \\n year","outputField":"payload","timeZone":"","storeName":"","commandResponseMsgOutput":"fanOut","defaultLocation":"","defaultLocationType":"default","outputs":6,"options":[{"name":"schedule1","topic":"minute","payloadType":"json","payload":"{\"fc\":4,\"address\":3000,\"quantity\":104}","expressionType":"cron","expression":"0 * * * * * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"},{"name":"schedule2","topic":"hour","payloadType":"json","payload":"{\"fc\":4,\"address\":3008,\"quantity\":2}","expressionType":"cron","expression":"0 0 * * * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"},{"name":"schedule3","topic":"month","payloadType":"json","payload":"{\"fc\":4,\"address\":3012,\"quantity\":2}","expressionType":"cron","expression":"0 10 0 * * MON#1 *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"},{"name":"schedule4","topic":"year","payloadType":"json","payload":"{\"fc\":4,\"address\":3018,\"quantity\":2}","expressionType":"cron","expression":"0 30 0 1 JAN * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"}],"x":670,"y":420,"wires":[["6fdf53e2377da215"],["92daab9a71d5358e"],["0c6472ade4af899d"],["0147f160697a0964"],[],[]]},{"id":"6fdf53e2377da215","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":1010,"y":360,"wires":[["1d5277b9a19cc57a"]]},{"id":"1d5277b9a19cc57a","type":"buffer-parser","z":"2592e3385ebb8175","name":"","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint16be","name":"power_production","offset":8,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"dc_voltage_current=>p1","offset":42,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"dc_voltage_current=>p2","offset":44,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"dc_voltage_current=>p3","offset":46,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"int16be","name":"cabinet_temperature","offset":82,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"grid_frequency","offset":84,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"fault_code","offset":86,"length":5,"offsetbit":0,"scale":"1","mask":""},{"type":"ascii","name":"status_text","offset":58,"length":16,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1210,"y":360,"wires":[["b41aca8f01762d20"]]},{"id":"b41aca8f01762d20","type":"debug","z":"2592e3385ebb8175","name":"debug 12","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1380,"y":360,"wires":[]},{"id":"07fa333bb03027e2","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule1","payloadType":"str","x":440,"y":360,"wires":[["da53f33028604d74"]]},{"id":"92daab9a71d5358e","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":1010,"y":400,"wires":[["8d5e783bc5cadb5e"]]},{"id":"8d5e783bc5cadb5e","type":"buffer-parser","z":"2592e3385ebb8175","name":"","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint32be","name":"energy_production","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1210,"y":400,"wires":[["b56029badc7fd33d"]]},{"id":"b56029badc7fd33d","type":"debug","z":"2592e3385ebb8175","name":"debug 13","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1380,"y":400,"wires":[]},{"id":"0c6472ade4af899d","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":1010,"y":440,"wires":[["49c7b8cb77a582dd"]]},{"id":"49c7b8cb77a582dd","type":"buffer-parser","z":"2592e3385ebb8175","name":"","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint32be","name":"energy_last_month","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1210,"y":440,"wires":[["0d673e916144e7aa"]]},{"id":"0d673e916144e7aa","type":"debug","z":"2592e3385ebb8175","name":"debug 14","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1380,"y":440,"wires":[]},{"id":"0147f160697a0964","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":1010,"y":480,"wires":[["3a8c84fe1a192bff"]]},{"id":"3a8c84fe1a192bff","type":"buffer-parser","z":"2592e3385ebb8175","name":"","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint32be","name":"energy_last_year","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1210,"y":480,"wires":[["da3a9f374cc78329"]]},{"id":"da3a9f374cc78329","type":"debug","z":"2592e3385ebb8175","name":"debug 15","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1380,"y":480,"wires":[]},{"id":"cce4d84144012840","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule2","payloadType":"str","x":440,"y":400,"wires":[["da53f33028604d74"]]},{"id":"bb3e748d684d4dc7","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule3","payloadType":"str","x":440,"y":440,"wires":[["da53f33028604d74"]]},{"id":"95443dc6f4433317","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule4","payloadType":"str","x":440,"y":480,"wires":[["da53f33028604d74"]]}]

Obviously this is untested and I dont know your data types but it should be useful from an approach POV.

1 Like

Yes that's it.Schedule X amount of messages. Read from modbus. Parse the read response. Then join all back together. Finally write data to remote resource (http). Before scheduling normal messages, a reset message is sent to join node to reset it (discard any incomplete join attempt from previous iteration).

All Buffer nodes output multiple values into a key pair sequence when possible.

I was thinking about possibly do all the buffer nodes sequentially, that way I could narrow the split and join to just around the modbus read node.

Perhaps I can explain what output I need at the start of the flow.

At every minute, I need a list of messages with modbus read configurations. At every hour, I need additional messages. And at higher time intervals too. For example:

  • Minute: [a, b, c]
  • Hour: [a, b, c, d, e]

Your approach has different streams per time interval. That's a lot of duplication when they all should at least partially do the same thing? Also I don't know about modbus, but having 4 read nodes to the same device could be risky? I think I read somewhere it was advicable to only use one. Further, is there no limit on how big range the register request can be? I don't think we can ask for the full range for example, if we need to the first and last value. Can't that be too much to handle for modbus?

I kinda did that, just better. every minute the flow grabs ALL the data in ONE GO and parses them out.

If you really want to do the reads separately, you could add a change node with an array of addresses and then a split node to make individual requests. Or, you could do them serially and combine the results at the end (demo below)


which is why i put it in a link-call subroutine


With link call, that is unnecessary. you could do it all serially.

[{"id":"da53f33028604d74","type":"cronplus","z":"2592e3385ebb8175","name":"minute \\n hour \\n month \\n year","outputField":"payload","timeZone":"","storeName":"","commandResponseMsgOutput":"fanOut","defaultLocation":"","defaultLocationType":"default","outputs":6,"options":[{"name":"schedule1","topic":"minute","payloadType":"json","payload":"{\"fc\":4,\"address\":3000,\"quantity\":104}","expressionType":"cron","expression":"0 * * * * * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"},{"name":"schedule2","topic":"hour","payloadType":"json","payload":"{\"fc\":4,\"address\":3008,\"quantity\":2}","expressionType":"cron","expression":"0 0 * * * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"},{"name":"schedule3","topic":"month","payloadType":"json","payload":"{\"fc\":4,\"address\":3012,\"quantity\":2}","expressionType":"cron","expression":"0 10 0 * * MON#1 *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"},{"name":"schedule4","topic":"year","payloadType":"json","payload":"{\"fc\":4,\"address\":3018,\"quantity\":2}","expressionType":"cron","expression":"0 30 0 1 JAN * *","location":"","offset":"0","solarType":"all","solarEvents":"sunrise,sunset"}],"x":670,"y":420,"wires":[["b6a14e529da80aab"],["92daab9a71d5358e"],["0c6472ade4af899d"],["0147f160697a0964"],[],[]]},{"id":"6fdf53e2377da215","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":1110,"y":360,"wires":[["1d5277b9a19cc57a"]]},{"id":"1d5277b9a19cc57a","type":"buffer-parser","z":"2592e3385ebb8175","name":"result1","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint32be","name":"power_production","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"result1","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1270,"y":360,"wires":[["11c3daaae7a7d301"]]},{"id":"07fa333bb03027e2","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule1","payloadType":"str","x":440,"y":360,"wires":[["da53f33028604d74"]]},{"id":"92daab9a71d5358e","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":910,"y":540,"wires":[["8d5e783bc5cadb5e"]]},{"id":"8d5e783bc5cadb5e","type":"buffer-parser","z":"2592e3385ebb8175","name":"","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint32be","name":"energy_production","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1110,"y":540,"wires":[["b56029badc7fd33d"]]},{"id":"b56029badc7fd33d","type":"debug","z":"2592e3385ebb8175","name":"debug 13","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1280,"y":540,"wires":[]},{"id":"0c6472ade4af899d","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":910,"y":580,"wires":[["49c7b8cb77a582dd"]]},{"id":"49c7b8cb77a582dd","type":"buffer-parser","z":"2592e3385ebb8175","name":"","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint32be","name":"energy_last_month","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1110,"y":580,"wires":[["0d673e916144e7aa"]]},{"id":"0d673e916144e7aa","type":"debug","z":"2592e3385ebb8175","name":"debug 14","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1280,"y":580,"wires":[]},{"id":"0147f160697a0964","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":910,"y":620,"wires":[["3a8c84fe1a192bff"]]},{"id":"3a8c84fe1a192bff","type":"buffer-parser","z":"2592e3385ebb8175","name":"","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint32be","name":"energy_last_year","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"payload","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1110,"y":620,"wires":[["da3a9f374cc78329"]]},{"id":"da3a9f374cc78329","type":"debug","z":"2592e3385ebb8175","name":"debug 15","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1280,"y":620,"wires":[]},{"id":"cce4d84144012840","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule2","payloadType":"str","x":440,"y":400,"wires":[["da53f33028604d74"]]},{"id":"bb3e748d684d4dc7","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule3","payloadType":"str","x":440,"y":440,"wires":[["da53f33028604d74"]]},{"id":"95443dc6f4433317","type":"inject","z":"2592e3385ebb8175","name":"trigger schedule","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"trigger","payload":"schedule4","payloadType":"str","x":440,"y":480,"wires":[["da53f33028604d74"]]},{"id":"b6a14e529da80aab","type":"change","z":"2592e3385ebb8175","name":"power_production","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"fc\":4,\"address\":3004,\"quantity\":2}","tot":"json"},{"t":"set","p":"topic","pt":"msg","to":"power_production","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":360,"wires":[["6fdf53e2377da215"]]},{"id":"adfa6b119a3f6199","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":1110,"y":400,"wires":[["969b04b44c8f2d57"]]},{"id":"969b04b44c8f2d57","type":"buffer-parser","z":"2592e3385ebb8175","name":"result2","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"uint16be","name":"dc_voltage_current=>p1","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"dc_voltage_current=>p2","offset":2,"length":1,"offsetbit":0,"scale":"1","mask":""},{"type":"uint16be","name":"dc_voltage_current=>p3","offset":4,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"result2","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1270,"y":400,"wires":[["bb0354df32eff847"]]},{"id":"11c3daaae7a7d301","type":"change","z":"2592e3385ebb8175","name":"dc_voltage_current","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"fc\":4,\"address\":3021,\"quantity\":8}","tot":"json"},{"t":"set","p":"topic","pt":"msg","to":"dc_voltage_current","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":900,"y":400,"wires":[["adfa6b119a3f6199"]]},{"id":"7bff34b2fc17c9a5","type":"link call","z":"2592e3385ebb8175","name":"","links":["9f92a8ce09c5e04b"],"linkType":"static","timeout":"30","x":1110,"y":440,"wires":[["32596e56c1090e3c"]]},{"id":"32596e56c1090e3c","type":"buffer-parser","z":"2592e3385ebb8175","name":"result3","data":"payload","dataType":"msg","specification":"spec","specificationType":"ui","items":[{"type":"int16be","name":"cabinet_temperature","offset":0,"length":1,"offsetbit":0,"scale":"1","mask":""}],"swap1":"","swap2":"","swap3":"","swap1Type":"swap","swap2Type":"swap","swap3Type":"swap","msgProperty":"result3","msgPropertyType":"str","resultType":"keyvalue","resultTypeType":"return","multipleResult":false,"fanOutMultipleResult":false,"setTopic":true,"outputs":1,"x":1270,"y":440,"wires":[["62a4c3e7022410b6"]]},{"id":"bb0354df32eff847","type":"change","z":"2592e3385ebb8175","name":"cabinet_temperature","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"fc\":4,\"address\":3041,\"quantity\":1}","tot":"json"},{"t":"set","p":"topic","pt":"msg","to":"cabinet_temperature","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":910,"y":440,"wires":[["7bff34b2fc17c9a5"]]},{"id":"b41aca8f01762d20","type":"debug","z":"2592e3385ebb8175","name":"debug 12","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1280,"y":480,"wires":[]},{"id":"62a4c3e7022410b6","type":"function","z":"2592e3385ebb8175","name":"combine","func":"msg.payload = {\n    ...msg.result1,\n    ...msg.result2,\n    ...msg.result3,\n    ...msg.result4,\n    ...msg.result5,\n    ...msg.result6\n}\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1140,"y":480,"wires":[["b41aca8f01762d20"]]},{"id":"ffc9684b738a0e36","type":"group","z":"2592e3385ebb8175","name":"Subroutine: get modbus data","style":{"label":true},"nodes":["9f92a8ce09c5e04b","59e412d3f7f3f8b8","ba2e4170bee7aadd"],"x":884,"y":699,"w":382,"h":82},{"id":"9f92a8ce09c5e04b","type":"link in","z":"2592e3385ebb8175","g":"ffc9684b738a0e36","name":"get from modbus","links":[],"x":925,"y":740,"wires":[["59e412d3f7f3f8b8"]]},{"id":"59e412d3f7f3f8b8","type":"function","z":"2592e3385ebb8175","g":"ffc9684b738a0e36","name":"fake modbus","func":"const quantity = msg.payload.quantity;\nconst array = [];\nfor (let i = 0; i < quantity; i++) {\n    array[i] = Math.floor(Math.random() * 32000)\n}\nmsg.payload = array\nreturn msg;","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1070,"y":740,"wires":[["ba2e4170bee7aadd"]]},{"id":"ba2e4170bee7aadd","type":"link out","z":"2592e3385ebb8175","g":"ffc9684b738a0e36","name":"link out 1","mode":"return","links":[],"x":1225,"y":740,"wires":[]}]

anyhow, I have given some ideas and my interpretation - if you find any of it useful or learn something, great.

1 Like

Ok those links are new to me, interesting! Learned something new for sure.

Problem is still, you get different output from minute and hour. So I would need to join those afterwards.

If minute has data for [a, b, c] and hour has data for [d, e], I need the following output (depending on time interval):

  • Minute: [a, b, c]
  • Hour: [a, b, c, d, e]

Which means I need schedules to trigger like a switch, only send one output even if there are multiple matches.

After a closer review, seems like in the 2nd example you must have a link call node per read configuration? So if I need to read 20 addresses (or ranges), it requires 20 link nodes, each with a separate buffer parser after and a prepare node at start. That's 60 nodes for 20 read requests!

In my example, I have them all in an array. 1 read node. At the cost of join logic, but at least it scales. I do however need 1 buffer parser node per prop, but that could possibly be combined if I prepared the read output and moved join back to before the buffer parser.

And as mentioned, the output of time intervals doesn't match in your example doesn't match my requirement.

Yes, thats why i said:

But the salient detail was

Does that not work for you?

To be clear - it reads 100 WORDS (200 bytes) in ONE operation (one link-call->modbus->return) then parses the 6 items you want. All instead of serial ops or multiple sends/requests.

To add a little flavour.

If what you are after is compact flows and minimal UI node configuration and dont care flow readability (as in Visual Flow readability), the amount of traffic/individual requests, reliability / data consistency, then you should carry on emitting sends from a function node and joining the results.

Perhaps a read of this article will change your mind? Modernize your legacy industrial data. Part 2. • FlowFuse

Allright, gotcha. Thanks for good explanation. I think the topic sort of split in two completely separate parts here. One is the scheduler, the other is the modbus read.

For scheduler, I'm not sure you realize the distinction in the output. Let's say time is 10:59:00. That's minutely schedule, so I want props:

  • 10:59 (MINUTE): [power_production, fault_code, grid_frequency, cabinet_temperature, dc_voltage_current, status_text]

1 minute later, time is 11:00:00. This is hourly schedule. Now I want props:

  • 11:00 (HOUR): [power_production, fault_code, grid_frequency, cabinet_temperature, dc_voltage_current, status_text, energy_production]

Do you see the difference? I fail to see how your suggestion supports that requirement.

For the sake of readability, this is perfectly readable in my eyes:

const requests = {
  minute: [
    { topic: "power_production",    value: 0, fc: 4, address: 3004, quantity:  2 }, // FC4 - 3005-06
    { topic: "fault_code",          value: 0, fc: 4, address: 3095, quantity:  5 }, // FC4 - 3096-3100
    { topic: "grid_frequency",      value: 0, fc: 4, address: 3042, quantity:  1 }, // FC4 - 3043
    { topic: "cabinet_temperature", value: 0, fc: 4, address: 3041, quantity:  1 }, // FC4 - 3042 (inverter temp)
    { topic: "dc_voltage_current",  value: 0, fc: 4, address: 3021, quantity:  8 }, // FC4 - 3022-29
    { topic: "status_text",         value: 0, fc: 4, address: 3029, quantity: 16 }, // FC4 - 3030-45 (alarm code and inverter status)
  ],
  hour: [
    { topic: "energy_production",   value: 0, fc: 4, address: 3008, quantity:  2 }, // FC4 - 3009-10
  ],
  month: [
    { topic: "energy_last_month",   value: 0, fc: 4, address: 3012, quantity:  2 }, // FC4 - 3013-14
  ],
  year: [
    { topic: "energy_last_year",    value: 0, fc: 4, address: 3018, quantity:  2 } // FC4 - 3019-20
  ]
};

But you are right it's less readable from visual flow perspective. I much prefer readability to be in code rather than visual flow here. It's all a balance of course.

I've read that article and taken inspiration from it. Instead of having multiple read nodes, I have one. Instead of multiple writes, I have single write. For read configuration, as shown above, could I merge all those requests into a single read with a range of 100? Something I need to look into for sure. That would actually solve all complexity here haha.

I played around with modbus, setting address to 3004, I was able to ask for a range of 77 addresses, but not 78. So there is the limit. This requires at minimum 2 requests to get all the props needed.

the number of reads supported is dependant both on the size of the buffer (Steve lists that as 100) and the device being read.

You appear to have a limit on your device of 77 registers - although that seems a strange number.

I read both my inverters and my power meters - it looks like you are querying an inverter here.

Not sure what the problem is - but you appear to be making it harder than it needs to be and more lialble to falure by using the join nodes.

Personally i would have a seperate CRON node for the different schedules (thats what i do) that you require and then feed the output into a Buffer parser node that outputs everything as a single message - you then take out what you need in a change node (for each category of data) and send that on to wherever you are displaying/storing them

Craig

2 Likes

Problem is simply this:

  • Minute: [a, b, c]
  • Hour: [a, b, c, d, e]

If separating schedules, output is 2 messages: [a,b,c] per minute and [d,e] per hour. I can simplify by sending 2x50 range requests via modbus, then join them and I have everything in one message for the buffer parser. So looks like it will be a lot simpler now. Not without joins though.

Why if they are getting different information on different timeframes do you want to join them ?

Why wouldn't the hourly job simply get d,e and the minute job get a,b,c every minute ?

Presumably you are inserting the records into some form of timeseries database (Influx etc ?)

Craig

Don't have to join them. But handling I/O on both sides will be easier, smoother and hopefully less error prone. I also find that later calculations becomes much more easier when having all data available in the same msg.

Thanks all, was able to greatly simplify this subflow to read from inverters:

Instead of reading and parsing individual values, the whole range of 96 records is read in 2 chunks (2x48). So I still need join, but just for 2 messages. It was very confusing to work with modbus register addresses in intervals of 2 bytes, while offset in buffer parser was 1 byte. But luckily it got a brilliant specification which can be coded, so was able to convert modbus addresses to byte offset (and add a bit of defaults too):

items.forEach((item) => {
  // convert modbus address (2-byte) to buffer parser offset (1-byte)
  // assumes request start address is off by one (starts one earlier than first item address)
  item.offset = (item.address - msg.startAddress - 1) * 2;
  delete item.address;
  item.length = item?.length ?? 1; // default 1
  item.offsetbit = item?.offsetbit ?? 0; // default 0
  item.scale = (item?.scale ?? "1") + ""; // convert to string, default "1"
    item.mask = (item?.mask ?? "") + ""; // convert to string, default "" (empty string)
});

Weird off-by-one for the modbus request (ask for 3004, but response seem to return addresses starting on 3005. The buffer parser was nice enough to spit out specification, so was a blast to implement from existing example.

Still had to sacrifice most of the crontab schedule syntax for javascript in order to get a single message on each minute interval:

let items = [];
const now = new Date(msg.ts);
if (now.getSeconds() === 0) {
  // add minute data
  items = [...items, ...minute];
}
if (now.getMinutes() === 0) {
  // add hour data
  items = [...items, ...hour];
}
if (now.getHours() === 0 && now.getMinutes() === 10 && now.getDate() === 1) {
  // add month data (00:10 on day 1 of each month)
  items = [...items, ...month];
}
if (
  now.getHours() === 0 &&
  now.getMinutes() === 30 &&
  now.getDate() === 1 &&
  now.getMonth() + 1 === 1
) {
  // add year data (00:30 on day 1 of january)
  items = [...items, ...year];
}

While this didn't really help anything on the errors, cutting down the amount of messages, I could increase retry attempts. So overall less errors and more stable data. I reckon modbus requests fails about 10%-30%!

1 Like