Hello everyone, I researched the nodes that exchange data in bacnet and tried the nodes in the node-red library but I didn't find them very successful. Which one do you use? Can you give me an example?
Can you elaborate on „not very successful“
We had same experience, all the available node red packages were unsuccessful. One of them looked promosing, but was unable to write some values (was it binary? don't remember). So we threw them all in the bin and used bacnet stack from instead. It is a program to run from cli, in our case linux. We could then run the command from exec node in node red. Unfortunately, this tool was not perfect either, but at least it mostly worked. The problem we found was that it could easily mix the responses when sending multiple queries, which is a critical problem of course. Solution was to schedule in node red so that it only sends one request at a time. Here is how the subflow looks like:
The source of the subflow is unfortunately too large to share here on forum. However here are the most of the nodes except the log subflow node:
[
{
"id": "cf9b7b3ebcecd1f9",
"type": "function",
"z": "6968fb5a019df2d0",
"name": "validate",
"func": "msg.attempt = 1;\nmsg.maxAttempts = msg.maxAttempts ?? env.get(\"maxAttempts\") ?? 1; // default 0 from config if not set\nmsg.delay = msg.delay ?? env.get(\"delay\") ?? 1; // default 0 from config if not set\nmsg.logWarning = msg.logWarning ?? env.get(\"logWarning\") ?? false;\nmsg.logError = msg.logError ?? env.get(\"logError\") ?? false;\nmsg.timeout = msg.timeout ?? env.get(\"timeout\") ?? 5;\n\ndelete msg.rate; // never accept incoming rates, this must be set based on timeout\nmsg.rate = (msg.timeout +1) * 1000; // rate (in milliseconds) needs to be higher than max timeout for command (in seconds)\nfunction isIntOrNull(value) {\n if (value == null) {\n return true;\n }\n value = parseInt(value, 10);\n if (Number.isInteger(value) && value > 0) {\n return true;\n }\n return false;\n}\n\nif(!isIntOrNull(msg.maxAttempts)){\n const log = `Invalid value for maxAttempts: ${msg.maxAttempts}`;\n node.error(log);\n throw new Error(log);\n}\n\nif (!isIntOrNull(msg.delay)) {\n const log = `Invalid delay value: ${msg.delay}`;\n node.error(log);\n throw new Error(log);\n}\n\nif (typeof msg.logWarning !== 'boolean'){\n msg.logWarning = false;\n}\n\nif (typeof msg.logError !== 'boolean') {\n msg.logError = false;\n}\n\n// default values\nmsg.maxAttempts = msg.maxAttempts ? msg.maxAttempts : 1; // default maxAttempt\nmsg.delay = msg.delay ? msg.delay * 1000 : 1000; // convert sec to ms, default 1000 ms\n\nmsg._backup = RED.util.cloneMessage(msg);\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 200,
"y": 240,
"wires": [
[
"794bac477da112e7"
]
]
},
{
"id": "9f24384b818d1b6f",
"type": "function",
"z": "6968fb5a019df2d0",
"name": "validate",
"func": "const errors = [];\nif(msg.method == \"read\"){\n if(msg?.payload?.length === 0 || msg?.payload === \"\"){\n // empty payload is only error for read\n errors.push(\"Empty payload.\");\n }\n}\n\nif(msg?.rc?.code !== 0){\n errors.push(`Return code: ${msg?.rc?.code}.`);\n}\nif (msg?.rc?.message){\n errors.push(msg.rc.message);\n}\nif(errors.length > 0 && msg?.payload){\n errors.push(msg.payload);\n}\n\nif(errors.length > 0){\n throw new Error(errors.join(\" \"));\n}\n\n// remove temporary bacnet config and retry junk\n// to avoid interference with other nodes or retry nodes downstream (after calling this subflow)\ndelete msg.method;\ndelete msg.attempt;\ndelete msg.maxAttempts;\ndelete msg.delay;\ndelete msg.logWarning;\ndelete msg.logError;\ndelete msg.logMessage;\ndelete msg._backup;\ndelete msg.rc;\ndelete msg.rate;\ndelete msg.timeout;\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1320,
"y": 220,
"wires": [
[]
]
},
{
"id": "8f9ba3316b446969",
"type": "status",
"z": "6968fb5a019df2d0",
"name": "",
"scope": [
"33c6f15ef6e8a5f5",
"276e027d7b4e56d2"
],
"x": 1400,
"y": 580,
"wires": [
[]
]
},
{
"id": "41c241697d5d9f5e",
"type": "junction",
"z": "6968fb5a019df2d0",
"x": 200,
"y": 360,
"wires": [
[
"794bac477da112e7"
]
]
},
{
"id": "5e2724bdc601025d",
"type": "junction",
"z": "6968fb5a019df2d0",
"x": 980,
"y": 440,
"wires": [
[
"41c241697d5d9f5e"
]
]
},
{
"id": "8287b5a8688bd1bc",
"type": "group",
"z": "6968fb5a019df2d0",
"name": "Command queue",
"style": {
"stroke": "#ffC000",
"fill": "#ffefbf",
"label": true,
"color": "#000000"
},
"nodes": [
"33c6f15ef6e8a5f5",
"36cda335f6059c8e",
"276e027d7b4e56d2",
"f99c1b0364d526e0",
"794bac477da112e7",
"bd87bba070798fcd",
"1b9e5ce89a3df516",
"95979eeab2857c9b",
"5655f010c7a3dc32",
"32c00d91c7609bf4"
],
"x": 294,
"y": 171.5,
"w": 912,
"h": 209.5
},
{
"id": "33c6f15ef6e8a5f5",
"type": "delay",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"name": "queue",
"pauseType": "rate",
"timeout": "5",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "15",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": true,
"outputs": 1,
"x": 970,
"y": 220,
"wires": [
[
"276e027d7b4e56d2"
]
]
},
{
"id": "36cda335f6059c8e",
"type": "function",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"name": "Flush",
"func": "return {flush: 1}",
"outputs": 1,
"timeout": "",
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 830,
"y": 260,
"wires": [
[
"33c6f15ef6e8a5f5"
]
]
},
{
"id": "276e027d7b4e56d2",
"type": "exec",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"command": "",
"addpay": "payload",
"append": "",
"useSpawn": "false",
"timer": "${timeout}",
"winHide": true,
"oldrc": false,
"name": "exec",
"x": 1090,
"y": 220,
"wires": [
[
"9f24384b818d1b6f"
],
[],
[
"32c00d91c7609bf4"
]
]
},
{
"id": "f99c1b0364d526e0",
"type": "function",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"name": "read",
"func": "if (Array.isArray(msg?.property)) {\n msg.payload = `bacnet-read-multiple ${msg.deviceId} ${msg.objectType} ${msg.objectInstance} `;\n for(const prop of msg.property){\n msg.payload += `${prop},`;\n }\n msg.payload = msg.payload.substring(0, msg.payload.length -1); // remove final comma at the end\n} else {\n msg.payload = `bacnet-read ${msg.deviceId} ${msg.objectType} ${msg.objectInstance} ${msg.property}`;\n}\n\n\n\nreturn msg;\n\n/*Example:\nAnalog Output object 99, use one of the following commands:\nCommand:readpropm deviceid:123 bacnet_properties:analog-output 99 85,87[0],87\nreadpropm 123 analog-output 99 85,87[0],87\nreadpropm 123 1 99 85,87[0],87\n*/",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 570,
"y": 220,
"wires": [
[
"5655f010c7a3dc32"
]
]
},
{
"id": "794bac477da112e7",
"type": "switch",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"name": "",
"property": "method",
"propertyType": "msg",
"rules": [
{
"t": "eq",
"v": "read",
"vt": "str"
},
{
"t": "eq",
"v": "write",
"vt": "str"
},
{
"t": "eq",
"v": "whois",
"vt": "str"
},
{
"t": "eq",
"v": "bacdiscover",
"vt": "str"
},
{
"t": "else"
}
],
"checkall": "false",
"repair": false,
"outputs": 5,
"x": 370,
"y": 240,
"wires": [
[
"f99c1b0364d526e0"
],
[
"bd87bba070798fcd"
],
[
"1b9e5ce89a3df516"
],
[
"95979eeab2857c9b"
],
[]
],
"outputLabels": [
"read",
"write",
"whois",
"",
"otherwise"
]
},
{
"id": "bd87bba070798fcd",
"type": "function",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"name": "write",
"func": "msg.payload = `bacnet-write ${msg.deviceId} ${msg.objectType} ${msg.objectInstance} ${msg.property} ${msg.priority} ${msg.index} ${msg.tag} ${msg.value}`;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 570,
"y": 260,
"wires": [
[
"5655f010c7a3dc32"
]
]
},
{
"id": "1b9e5ce89a3df516",
"type": "function",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"name": "whois",
"func": "msg.payload = `bacnet-whois`;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 570,
"y": 300,
"wires": [
[
"5655f010c7a3dc32"
]
]
},
{
"id": "95979eeab2857c9b",
"type": "function",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"name": "bacdiscover",
"func": "msg.payload = `bacnet-discover`;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 590,
"y": 340,
"wires": [
[
"5655f010c7a3dc32"
]
]
},
{
"id": "5655f010c7a3dc32",
"type": "junction",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"x": 740,
"y": 220,
"wires": [
[
"33c6f15ef6e8a5f5"
]
]
},
{
"id": "32c00d91c7609bf4",
"type": "junction",
"z": "6968fb5a019df2d0",
"g": "8287b5a8688bd1bc",
"x": 1180,
"y": 260,
"wires": [
[
"36cda335f6059c8e"
]
]
},
{
"id": "cf2095c134878bdc",
"type": "group",
"z": "6968fb5a019df2d0",
"name": "Retry?",
"style": {
"stroke": "#0070c0",
"fill": "#bfdbef",
"label": true,
"color": "#000000"
},
"nodes": [
"edea7d6f49d9c884",
"058214040a1bf485",
"a646d8d8c6dd4085",
"54bdb9ac16624e82",
"3d89135e65b83091"
],
"x": 294,
"y": 439,
"w": 632,
"h": 122
},
{
"id": "a5184c5aab2f6cda",
"type": "subflow",
"name": "log",
"info": "# Writes log to file\r\nMust set env variable `logTopic` in parent flow to determine filename.\r\nThis can't be done as input variable because the parent may also be a subflow.\r\n\r\n### Inputs\r\n\r\n: logMessage (string) : the log message to write to file can be provided in the node configuration, or as a message property. By default it will use `msg.logMessage`.\r\n: logTopic (string) : determines parts of the file name. Typically enter flow name for this purpose.\r\n: logCompleteMsgObj (boolean): Add the entire msg object to the log message.\r\n: stdOut (boolean) : Also log to standard out (using debug node).\r\n\r\n### Details\r\n\r\nLogs are written to /data/logs/`msg.logTopic`_yyyy-MM-dd.txt. `msg.logTopic` is used as filename prefix. Typically use flow name for this purpose. Creates file and folder if they don't exist.\r\n\r\n`msg.logMessage` is used as the main content of the log to write. Datetime is automatically added as prefix. Each log is appended with newline.",
"category": "",
"in": [
{
"x": 100,
"y": 80,
"wires": [
{
"id": "0db769e63c22272e"
}
]
}
],
"out": [
{
"x": 1040,
"y": 80,
"wires": [
{
"id": "5f7d3b7030f7bb02",
"port": 0
},
{
"id": "0db769e63c22272e",
"port": 0
}
]
}
],
"env": [
{
"name": "logMessage",
"type": "str",
"value": "",
"ui": {
"label": {
"en-US": "Log message"
},
"type": "input",
"opts": {
"types": [
"str"
]
}
}
},
{
"name": "logCompleteMsgObj",
"type": "bool",
"value": "false",
"ui": {
"label": {
"en-US": "Log complete msg"
},
"type": "checkbox"
}
},
{
"name": "stdOut",
"type": "bool",
"value": "false",
"ui": {
"label": {
"en-US": "Debug (sidebar)"
},
"type": "checkbox"
}
},
{
"name": "logTopic",
"type": "env",
"value": "$parent.logTopic",
"ui": {
"type": "hide"
}
}
],
"meta": {},
"color": "#C7E9C0",
"icon": "font-awesome/fa-file-text"
},
{
"id": "b1add847796af765",
"type": "function",
"z": "a5184c5aab2f6cda",
"name": "prepare log",
"func": "msg._backup = RED.util.cloneMessage(msg);\n\nconst date = new Date().toISOString().replaceAll(\"-\", \"-\").replaceAll(\"T\", \"-\").replaceAll(\":\", \"-\").replaceAll(\".\", \"-\").replaceAll(\"Z\", \"-\");\nconst list = date.split(\"-\");\nconst year = list[0];\nconst month = list[1];\nconst day = list[2];\nconst hour = list[3];\nconst minute = list[4];\nconst second = list[5];\n\nlet logTopic;\ntry{\n logTopic = env.get(\"logTopic\").toLowerCase();\n if(!logTopic){\n node.error(\"ERROR: Log failed getting logTopic env from parent. Did you remember to set it?\");\n return;\n }\n} catch (error) {\n node.error(\"ERROR: Log failed getting logTopic env from parent. Did you remember to set it?\");\n return;\n}\n\nmsg.stdOut = env.get(\"stdOut\");\n\nlet logMessage = env.get(\"logMessage\") ? env.get(\"logMessage\") : msg.logMessage;\nlet logCompleteMsgObj = env.get(\"logCompleteMsgObj\");\nif(!logCompleteMsgObj){\n logCompleteMsgObj = msg?.logCompleteMsgObj ?? false;\n}\n\nif(logCompleteMsgObj){\n logMessage += \"\\n\";\n logMessage += JSON.stringify(msg._backup);\n}\n\nmsg.filename = `/data/logs/${logTopic}/${logTopic}_${year}-${month}-${day}.log`;\nmsg.payload = `${year}-${month}-${day} ${hour}:${minute}:${second} - ${logMessage}`;\n\nconst maxLineLength = 10000;\nif(msg.payload.length > maxLineLength){\n // Safeguard against excessively large logs\n msg.payload = msg.payload.slice(0, maxLineLength) + '... [TRUNCATED]';\n}\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 470,
"y": 140,
"wires": [
[
"3ea911242530d241",
"2c7e47352b7df809"
]
]
},
{
"id": "3ea911242530d241",
"type": "file",
"z": "a5184c5aab2f6cda",
"name": "write log file",
"filename": "filename",
"filenameType": "msg",
"appendNewline": true,
"createDir": true,
"overwriteFile": "false",
"encoding": "utf8",
"x": 670,
"y": 140,
"wires": [
[
"5f7d3b7030f7bb02"
]
]
},
{
"id": "e96bad283c240487",
"type": "catch",
"z": "a5184c5aab2f6cda",
"name": "",
"scope": null,
"uncaught": false,
"x": 660,
"y": 260,
"wires": [
[
"5ac9480a70f4d93a"
]
]
},
{
"id": "5ac9480a70f4d93a",
"type": "debug",
"z": "a5184c5aab2f6cda",
"name": "log failed error",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 850,
"y": 260,
"wires": []
},
{
"id": "fac2f56d19607dc3",
"type": "debug",
"z": "a5184c5aab2f6cda",
"name": "log std out",
"active": true,
"tosidebar": true,
"console": true,
"tostatus": false,
"complete": "payload",
"targetType": "msg",
"statusVal": "",
"statusType": "auto",
"x": 830,
"y": 200,
"wires": []
},
{
"id": "2c7e47352b7df809",
"type": "switch",
"z": "a5184c5aab2f6cda",
"name": "std out?",
"property": "stdOut",
"propertyType": "msg",
"rules": [
{
"t": "true"
},
{
"t": "else"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 660,
"y": 200,
"wires": [
[
"fac2f56d19607dc3"
],
[]
],
"inputLabels": [
"msg.stdOut"
],
"outputLabels": [
"true",
"false"
]
},
{
"id": "5f7d3b7030f7bb02",
"type": "function",
"z": "a5184c5aab2f6cda",
"name": "revert",
"func": "return msg._backup;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 810,
"y": 140,
"wires": [
[]
]
},
{
"id": "0db769e63c22272e",
"type": "function",
"z": "a5184c5aab2f6cda",
"name": "logMessage?",
"func": "const logMessage = env.get(\"logMessage\") ? env.get(\"logMessage\") : msg.logMessage;\n\nconst skip = logMessage ? null : msg;\nconst log = logMessage ? msg : null;\nreturn [skip, log];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 220,
"y": 80,
"wires": [
[],
[
"b1add847796af765"
]
],
"outputLabels": [
"Skip",
"Log"
]
},
{
"id": "71082c29770ea643",
"type": "inject",
"z": "a5184c5aab2f6cda",
"g": "6f8fb577b11364ed",
"name": "",
"props": [],
"repeat": "",
"crontab": "00 00 * * *",
"once": false,
"onceDelay": 0.1,
"topic": "",
"x": 210,
"y": 400,
"wires": [
[
"09a9a5f729dd65b4"
]
]
},
{
"id": "0c31a507622aeb8d",
"type": "file",
"z": "a5184c5aab2f6cda",
"g": "6f8fb577b11364ed",
"name": "write empty file",
"filename": "filename",
"filenameType": "msg",
"appendNewline": false,
"createDir": true,
"overwriteFile": "false",
"encoding": "utf8",
"x": 540,
"y": 400,
"wires": [
[]
]
},
{
"id": "09a9a5f729dd65b4",
"type": "function",
"z": "a5184c5aab2f6cda",
"g": "6f8fb577b11364ed",
"name": "empty log",
"func": "const date = new Date().toISOString().replaceAll(\"-\", \"-\").replaceAll(\"T\", \"-\").replaceAll(\":\", \"-\").replaceAll(\".\", \"-\").replaceAll(\"Z\", \"-\");\nconst list = date.split(\"-\");\nconst year = list[0]; // YYYY\nconst month = list[1]; // MM\nconst day = list[2]; // DD\n\nconst logTopic = env.get(\"$parent.logTopic\").toLowerCase();\nif(!logTopic){\n return; // silent quit\n}\n\nmsg.filename = `/data/logs/${logTopic}/${logTopic}_${year}-${month}-${day}.log`;\nmsg.payload = \"\";\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 370,
"y": 400,
"wires": [
[
"0c31a507622aeb8d"
]
]
},
{
"id": "6f8fb577b11364ed",
"type": "group",
"z": "a5184c5aab2f6cda",
"name": "Create new log file every day",
"style": {
"stroke": "#0070c0",
"fill": "#bfdbef",
"label": true,
"color": "#000000"
},
"nodes": [
"0c31a507622aeb8d",
"09a9a5f729dd65b4",
"71082c29770ea643"
],
"x": 114,
"y": 359,
"w": 532,
"h": 82
},
{
"id": "edea7d6f49d9c884",
"type": "catch",
"z": "6968fb5a019df2d0",
"g": "cf2095c134878bdc",
"name": "",
"scope": [
"8c79a06a2e910e32",
"9f24384b818d1b6f"
],
"uncaught": false,
"x": 370,
"y": 500,
"wires": [
[
"a646d8d8c6dd4085"
]
]
},
{
"id": "058214040a1bf485",
"type": "subflow:a5184c5aab2f6cda",
"z": "6968fb5a019df2d0",
"g": "cf2095c134878bdc",
"name": "Log Error",
"env": [
{
"name": "stdOut",
"value": "false",
"type": "bool"
}
],
"x": 640,
"y": 520,
"wires": [
[]
]
},
{
"id": "a646d8d8c6dd4085",
"type": "function",
"z": "6968fb5a019df2d0",
"g": "cf2095c134878bdc",
"name": "retry?",
"func": "function cleanUpLog(){\n if(msg.logMessage){\n msg.logMessage = msg.logMessage.replaceAll(\"\\n\", \" \"); // remove new lines\n msg.logMessage = msg.logMessage.replace(/\\s+/g, \" \"); // reduce multiple spaces to one space\n }\n}\n\nif (msg.attempt >= msg.maxAttempts) {\n msg.logMessage = \"\";\n if(msg.logError){\n if (msg.maxAttempts > 1) {\n msg.logMessage = `ERROR: Bacnet failed ${msg.method} ${msg?.topic} (${msg?.error?.message}) after ${msg.maxAttempts} attempts.`;\n } else {\n msg.logMessage = `ERROR: Bacnet failed ${msg.method} ${msg?.topic} (${msg?.error?.message}).`;\n }\n if(msg?.rc?.signal == \"SIGTERM\"){\n msg.logMessage += ` Command timeout after ${msg.timeout} seconds (SIGTERM).`;\n }\n cleanUpLog();\n }\n return [null, msg];\n}\nconst attempt = msg.attempt;\n\nlet error = msg?.error?.message;\nif(error && \"error: \".toLowerCase().startsWith(\"error: \")){\n error = error.replace(/^error: /, \"\").trim();\n}\n\nlet logMessage = \"\";\nif(msg.logWarning){\n logMessage = `WARNING: Bacnet failed ${msg.method} ${msg.topic} (${error}), attempt nr ${msg.attempt}.`;\n if(msg?.rc?.signal == \"SIGTERM\"){\n logMessage += ` Command timeout after ${msg.timeout} seconds (SIGTERM).`;\n }\n logMessage += ` Trying again in ${msg.delay / 1000} seconds.`;\n}\n\n\nlogMessage += ``;\nmsg = msg._backup; // revert to original message\nmsg._backup = RED.util.cloneMessage(msg); // reapply backup\n\nmsg.logMessage = logMessage;\ncleanUpLog();\nmsg.attempt = attempt +1;\n\nreturn [msg, null];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 490,
"y": 500,
"wires": [
[
"54bdb9ac16624e82"
],
[
"058214040a1bf485"
]
],
"outputLabels": [
"Retry",
"No retry"
]
},
{
"id": "54bdb9ac16624e82",
"type": "subflow:a5184c5aab2f6cda",
"z": "6968fb5a019df2d0",
"g": "cf2095c134878bdc",
"name": "Log Warning (retry)",
"x": 670,
"y": 480,
"wires": [
[
"3d89135e65b83091"
]
]
},
{
"id": "3d89135e65b83091",
"type": "delay",
"z": "6968fb5a019df2d0",
"g": "cf2095c134878bdc",
"name": "",
"pauseType": "delayv",
"timeout": "1",
"timeoutUnits": "seconds",
"rate": "1",
"nbRateUnits": "1",
"rateUnits": "second",
"randomFirst": "1",
"randomLast": "5",
"randomUnits": "seconds",
"drop": false,
"allowrate": false,
"outputs": 1,
"x": 840,
"y": 480,
"wires": [
[
"5e2724bdc601025d"
]
]
}
]
And here is the external bacnet program: