UNPKG

@montagny/node-red-contrib-lorawan-bacnet

Version:

Provides custom Node-RED nodes for integrating LoRaWAN devices with BACnet systems

948 lines 65.7 kB
[ { "id": "d5185b34c44ae683", "type": "tab", "label": "LoRaBAC", "disabled": false, "info": "", "env": [] }, { "id": "e5621a7de149225c", "type": "subflow", "name": "Rest API Read downlink/ Write uplink", "info": "", "category": "", "in": [ { "x": 300, "y": 360, "wires": [ { "id": "8823ed2c61e64b0f" }, { "id": "580fb662e924a681" } ] } ], "out": [ { "x": 1540, "y": 460, "wires": [ { "id": "f8a6a26c3a4274bf", "port": 0 } ] } ], "env": [ { "name": "dataDirection", "type": "str", "value": "uplink", "ui": { "icon": "font-awesome/fa-location-arrow", "label": { "en-US": "Direction" }, "type": "select", "opts": { "opts": [ { "l": { "en-US": "Uplink" }, "v": "uplink" }, { "l": { "en-US": "Downlink" }, "v": "downlink" } ] } } } ], "meta": {}, "color": "#DDAA99", "status": { "x": 660, "y": 580, "wires": [ { "id": "c5511cd0867d06e2", "port": 0 } ] } }, { "id": "b3c9c070e14e7188", "type": "group", "z": "e5621a7de149225c", "name": "Read/Write BACnet Objects", "style": { "stroke": "#ff0000", "fill": "#ffbfbf", "label": true, "color": "#000000" }, "nodes": [ "884f11face673e2c", "8823ed2c61e64b0f" ], "x": 394, "y": 319, "w": 492, "h": 82 }, { "id": "58e5886e40ebacd1", "type": "group", "z": "e5621a7de149225c", "name": "Create missing BACnet Objects", "style": { "stroke": "#ff0000", "fill": "#ffbfbf", "label": true, "color": "#000000" }, "nodes": [ "8e36bdc54932c785", "4671414315b782ca", "11c02edca91f42f3", "0a876338e2be6dd8", "5e76761149f4280e", "d04fc338048162ec", "d7fb76702f1ebdab" ], "x": 1154, "y": 219, "w": 762, "h": 142 }, { "id": "cfdfc86845dc9bb9", "type": "group", "z": "d5185b34c44ae683", "name": "Uplink", "style": { "label": true, "color": "#000000", "fill": "#e3f3d3" }, "nodes": [ "6004507d410bec08", "5338fce50ecc57d9", "cfaebde9dbc1901d", "2694240cdee88a83" ], "x": 14, "y": 219, "w": 612, "h": 162 }, { "id": "90c82936d1f21eda", "type": "group", "z": "d5185b34c44ae683", "name": "Downlink", "style": { "fill": "#e3f3d3", "label": true, "color": "#000000" }, "nodes": [ "4fbd348dfae7d9ce", "8ef7bc0b865b1230", "a6ff13a9089b4272", "adb0c33fcb917696" ], "x": 634, "y": 219, "w": 712, "h": 162 }, { "id": "c5511cd0867d06e2", "type": "junction", "z": "e5621a7de149225c", "x": 600, "y": 580, "wires": [ [] ] }, { "id": "f8a6a26c3a4274bf", "type": "junction", "z": "e5621a7de149225c", "x": 1460, "y": 460, "wires": [ [ "a1b40b26fe6dd41f" ] ] }, { "id": "914dac3cf2194eb6", "type": "mqtt-broker", "name": "The Things Network", "broker": "eu1.cloud.thethings.network", "port": 1883, "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": 4, "keepalive": 60, "cleansession": true, "autoUnsubscribe": true, "birthTopic": "", "birthQos": "0", "birthRetain": "false", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closeQos": "0", "closeRetain": "false", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willRetain": "false", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" }, { "id": "74dfa9d48cf1b5c8", "type": "mqtt-broker", "name": "mosquitto local (docker)", "broker": "mosquitto", "port": 1883, "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": 4, "keepalive": 60, "cleansession": true, "autoUnsubscribe": true, "birthTopic": "", "birthQos": "0", "birthRetain": "false", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closeQos": "0", "closeRetain": "false", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willRetain": "false", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" }, { "id": "5f754ddd5c5bce24", "type": "mqtt-broker", "name": "The Things Stack", "broker": "stack", "port": "1883", "tls": "", "clientid": "", "autoConnect": true, "usetls": false, "protocolVersion": "4", "keepalive": "60", "cleansession": true, "autoUnsubscribe": true, "birthTopic": "", "birthQos": "0", "birthRetain": "false", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closeQos": "0", "closeRetain": "false", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willRetain": "false", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" }, { "id": "2418490b3512393e", "type": "tls-config", "name": "", "cert": "", "key": "", "ca": "", "certname": "", "keyname": "", "caname": "", "servername": "", "verifyservercert": false, "alpnprotocol": "" }, { "id": "d6dd3de173c23998", "type": "mqtt-broker", "name": "Chirpstack-Cloud", "broker": "chirpstack.univ-lorawan.fr", "port": "8883", "tls": "2418490b3512393e", "clientid": "", "autoConnect": true, "usetls": true, "protocolVersion": "4", "keepalive": "60", "cleansession": true, "autoUnsubscribe": true, "birthTopic": "", "birthQos": "0", "birthRetain": "false", "birthPayload": "", "birthMsg": {}, "closeTopic": "", "closeQos": "0", "closeRetain": "false", "closePayload": "", "closeMsg": {}, "willTopic": "", "willQos": "0", "willRetain": "false", "willPayload": "", "willMsg": {}, "userProps": "", "sessionExpiry": "" }, { "id": "fa7f1bf4647d5456", "type": "function", "z": "e5621a7de149225c", "name": "BACnet Object Exist ?", "func": "let device = msg.device;\nconst debug = flow.get(\"$parent.g_debug\");\n\nswitch (msg.statusCode) {\n ////////////////////////////////////////////////// \n // Case 200 : \"Success\" > Stops here OR continue to read Downlink Objects. \n // Case 200 : \"Object does not exist\" > Create Objects\n //////////////////////////////////////////////////\n case 200:\n if (msg.payload.includes(\"Unknown Object\")) {\n debug(device, \"creation\", `${device.identity.deviceName} : Some BACnet objects don't exist`);\n return [{ device: device }, null]; // Create Downlink Objects\n }\n else {\n switch (env.get(\"dataDirection\")) {\n case \"uplink\":\n debug(device, \"up\", `${device.identity.deviceName} (RestAPI) : Write Uplink Objects`);\n\n const dataDirection = Object.values(device.bacnet.objects).map(obj => obj.dataDirection);\n\n if (dataDirection.some(direction => { return direction === \"downlink\" })) {\n return [null, { device: device }]; // Continue to read downlink Objects\n }\n else {\n debug(device, \"txTime\", `${device.identity.deviceName} (${device.controller.protocol}) : TX time = ${Date.now() - device.transmitTime} ms`);\n return [null, null]; // Stops here\n }\n break;\n case \"downlink\":\n debug(device, \"down\", `${device.identity.deviceName} (RestAPI) : Read Downlink Objects`);\n return [null, { device: device, payload: JSON.parse(msg.payload) }];\n break;\n default:\n \n }\n \n }\n\n case 400:\n node.error(\"Error : Bad HTTP Request\");\n if (msg.payload.includes(\"write-access-denied\")) {\n node.error(\"Error : Trying to write a Read Only object (analogInput)\");\n }\n return [null, null];\n\n case 401:\n node.error(\"Error : Can't connect to controller : Authorization error\");\n return [null, null];\n\n case 500:\n node.error(\"Error : Server Error 500\");\n return [null, null];\n\n case 404:\n node.error(\"Error : 404\");\n return [null, null];\n\n case \"ETIMEDOUT\":\n node.error(\"Error : Can't connect to controller : TimeOut\");\n return [null, null];\n\n case \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\":\n node.error(\"Error : You forgot to enable the TLS config in your HTTP node\");\n return [null, null];\n\n}\n", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1000, "y": 360, "wires": [ [ "8e36bdc54932c785", "780a20b5b444c3d7" ], [ "8118aa09800ae8ee" ] ], "icon": "node-red/switch.svg" }, { "id": "884f11face673e2c", "type": "http request", "z": "e5621a7de149225c", "g": "b3c9c070e14e7188", "name": "HTTP REQUEST", "method": "use", "ret": "txt", "paytoqs": "ignore", "url": "", "tls": "2418490b3512393e", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 770, "y": 360, "wires": [ [ "fa7f1bf4647d5456" ] ] }, { "id": "8823ed2c61e64b0f", "type": "function", "z": "e5621a7de149225c", "g": "b3c9c070e14e7188", "name": "READ/WRITE Objects", "func": "\nlet device = msg.device;\nlet dataDirection = env.get('dataDirection')\nlet bacnetObjects = device?.bacnet?.objects;\n\nif(device?.controller?.protocol != \"restAPIBacnet\") return null;\n\nswitch (device?.controller?.model) {\n\n ///////////////////////////////////////////////////////////\n ////// Distech Controls Eclypse APEX\n ////// https://www.postman.com/distech/distech-ecy-v2-public/request/3qk28wy/write-property-multiple\n ///////////////////////////////////////////////////////////\n case \"distechControlsV2\":\n /********* HTTP Request Write Properties\n {\n \"method\": \"POST\",\n \"url\": \"https://@IP/api/rest/v2/services/bacnet/local/objects/write-property-multiple\",\n \"headers\": {Authorization: httpAuthentication,\n ContentType: \"application/json\"},\n \n \"payload\": {\n \"encode\": \"text\",\n \"property-references\": [\n {\n \"type\": \"analogValue\",\n \"instance\": y,\n \"property\": \"presentValue\",\n \"value\" : \"xx\"\n },\n {\n \"type\": \"analogValue\",\n \"instance\": y,\n \"property\": \"presentValue\",\n \"value\" : \"xx\"\n },\n ...\n ]\n },\n \"requestTimeout\" : xxx (ms)\n }\n */\n\n let property_references = [];\n for (let object in bacnetObjects) {\n if (bacnetObjects[object].dataDirection == dataDirection) {\n let temp = {}\n switch (dataDirection) {\n case \"uplink\":\n temp = '{ \"type\": \"' + bacnetObjects[object].objectType + '\", \"instance\": ' + bacnetObjects[object].instanceNum + ', \"property\": \"presentValue\", \"value\": ' + bacnetObjects[object].value + ' }';\n property_references.push(JSON.parse(temp));\n break;\n case \"downlink\":\n temp = '{ \"type\": \"' + bacnetObjects[object].objectType + '\", \"instance\": ' + bacnetObjects[object].instanceNum + ', \"property\": \"presentValue\"}';\n property_references.push(JSON.parse(temp));\n break;\n default:\n \n }\n }\n }\n\n // Return HTTP Request\n switch (dataDirection) {\n case \"uplink\":\n return {\n \"method\": \"POST\",\n \"url\": \"https://\" + device.controller.ipAddress + \"/api/rest/v2/services/bacnet/local/objects/write-property-multiple\",\n \"headers\": {\n Authorization: device.controller.httpAuthentication,\n ContentType: \"application/json\"\n },\n \"payload\": {\n \"encode\": \"text\",\n \"property-references\": property_references\n },\n \"requestTimeout\": flow.get('$parent.g_httpRequestTimeOut'),\n \"device\": device\n }\n break;\n case \"downlink\":\n return {\n \"method\": \"POST\",\n \"url\": \"https://\" + device.controller.ipAddress + \"/api/rest/v2/services/bacnet/local/objects/read-property-multiple\",\n \"headers\": {\n Authorization: device.controller.httpAuthentication,\n ContentType: \"application/json\"\n },\n \"payload\": {\n \"encode\": \"text\",\n \"property-references\": property_references\n },\n \"requestTimeout\": flow.get('$parent.g_httpRequestTimeOut'),\n \"device\": device\n }\n break;\n default:\n return\n break;\n }\n \n\n\n ///////////////////////////////////////////////////////////\n ////// XXXXX Controller\n ////// URL to the API documentation\n ///////////////////////////////////////////////////////////\n case \"anotherController\":\n\n break;\n default:\n \n return null;\n break;\n}\n\n\n\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 520, "y": 360, "wires": [ [ "884f11face673e2c" ] ] }, { "id": "8e36bdc54932c785", "type": "function", "z": "e5621a7de149225c", "g": "58e5886e40ebacd1", "name": "CREATE Objects", "func": "\nlet device = msg.device;\nlet bacnetObjects = device.bacnet.objects;\n\nswitch (device.controller.model) {\n\n ///////////////////////////////////////////////////////////\n ////// Distech Controls Eclypse APEX\n ////// https://www.postman.com/distech/distech-ecy-v2-public/request/57jbx8w/create-objects-multiple\n ///////////////////////////////////////////////////////////\n case \"distechControlsV2\":\n\n /********** Objects creation on the controller\n {\n \"method\": \"POST\",\n \"url\": \"https://\" + flow.get('$parent.g_controllerIP') +\"/api/rest/v2/batch\",\n \"headers\": {Authorization: flow.get('$parent.g_httpAuthentication'),\n ContentType: \"application/json\"}\n \"payload\":{\n \"requests\": [\n {\n \"id\": \"1\",\n \"method\": \"POST\",\n \"url\": \"/api/rest/v2/services/bacnet/local/objects/add\",\n \"body\": {\n \"object-type\": \"AnalogValue\",\n \"instance-number\": 10010,\n \"name\": \"apiAVTest10\"\n }\n },\n {\n \"id\": \"2\",\n \"method\": \"POST\",\n \"url\": \"/api/rest/v2/services/bacnet/local/objects/add\",\n \"body\": {\n \"object-type\": \"BinaryValue\",\n \"instance-number\": 10010,\n \"name\": \"apiBVTest10\"\n }\n },\n ...\n ]\n },\n \"requestTimeout\" : xxx (ms)\n }\n */\n\n\n let requests = [], i = 1;\n\n for (let object in bacnetObjects) {\n let temp = '{ \"id\": \"' + (i++) + '\", \"method\": \"POST\", \"url\": \"/api/rest/v2/services/bacnet/local/objects/add\", \"body\": { \"object-type\": \"' + bacnetObjects[object].objectType + '\", \"instance-number\": ' + bacnetObjects[object].instanceNum + ', \"name\": \"' + bacnetObjects[object].objectName + '\" } }';\n requests.push(JSON.parse(temp));\n }\n\n // Return HTTP Request\n return {\n \"method\": \"POST\",\n \"url\": \"https://\" + device.controller.ipAddress + \"/api/rest/v2/batch\",\n \"headers\": {\n Authorization: device.controller.httpAuthentication,\n ContentType: \"application/json\"\n },\n \"payload\": { \"requests\": requests },\n \"requestTimeout\": flow.get('$parent.g_httpRequestTimeOut'),\n \"device\": device\n }\n\n ///////////////////////////////////////////////////////////\n ////// XXXXX Controller\n ////// URL to the API documentation\n ///////////////////////////////////////////////////////////\n case \"anotherController\":\n\n \n}", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1270, "y": 260, "wires": [ [ "4671414315b782ca" ] ] }, { "id": "4671414315b782ca", "type": "http request", "z": "e5621a7de149225c", "g": "58e5886e40ebacd1", "name": "HTTP REQUEST", "method": "use", "ret": "txt", "paytoqs": "ignore", "url": "", "tls": "2418490b3512393e", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 1550, "y": 260, "wires": [ [ "11c02edca91f42f3", "5e76761149f4280e" ] ] }, { "id": "11c02edca91f42f3", "type": "function", "z": "e5621a7de149225c", "g": "58e5886e40ebacd1", "name": "READ/WRITE Objects", "func": "\nlet device = msg.device;\nlet previousValues = flow.get(\"$parent.g_previousValues\");\nlet dataDirection = env.get('dataDirection')\nlet bacnetObjects = device.bacnet.objects;\n\nswitch (device.controller.model) {\n\n ///////////////////////////////////////////////////////////\n ////// Distech Controls Eclypse APEX\n ////// https://www.postman.com/distech/distech-ecy-v2-public/request/3qk28wy/write-property-multiple\n ///////////////////////////////////////////////////////////\n case \"distechControlsV2\":\n /********* HTTP Request Write Properties\n {\n \"method\": \"POST\",\n \"url\": \"https://@IP/api/rest/v2/services/bacnet/local/objects/write-property-multiple\",\n \"headers\": {Authorization: httpAuthentication,\n ContentType: \"application/json\"},\n \n \"payload\": {\n \"encode\": \"text\",\n \"property-references\": [\n {\n \"type\": \"analogValue\",\n \"instance\": y,\n \"property\": \"presentValue\",\n \"value\" : \"xx\"\n },\n {\n \"type\": \"analogValue\",\n \"instance\": y,\n \"property\": \"presentValue\",\n \"value\" : \"xx\"\n },\n ...\n ]\n },\n \"requestTimeout\" : xxx (ms)\n }\n */\n\n let property_references = [];\n for (let object in bacnetObjects) {\n let temp = {}\n switch (bacnetObjects[object].dataDirection) {\n case \"uplink\":\n temp = '{ \"type\": \"' + bacnetObjects[object].objectType + '\", \"instance\": ' + bacnetObjects[object].instanceNum + ', \"property\": \"presentValue\", \"value\": ' + bacnetObjects[object].value + ' }';\n property_references.push(JSON.parse(temp));\n break;\n case \"downlink\":\n // if it exist take the previous value of the downlink BACnet object\n if (previousValues.hasOwnProperty(device.identity.deviceName)){\n temp = '{ \"type\": \"' + bacnetObjects[object].objectType + '\", \"instance\": ' + bacnetObjects[object].instanceNum + ', \"property\": \"presentValue\", \"value\": ' + previousValues[device.identity.deviceName].bacnet.objects[object].value + ' }';\n property_references.push(JSON.parse(temp));\n }else{\n temp = '{ \"type\": \"' + bacnetObjects[object].objectType + '\", \"instance\": ' + bacnetObjects[object].instanceNum + ', \"property\": \"presentValue\", \"value\": ' + bacnetObjects[object].value + ' }';\n property_references.push(JSON.parse(temp));\n }\n\n break;\n default:\n }\n \n }\n\n // Return HTTP Request\n return {\n \"method\": \"POST\",\n \"url\": \"https://\" + device.controller.ipAddress + \"/api/rest/v2/services/bacnet/local/objects/write-property-multiple\",\n \"headers\": {\n Authorization: device.controller.httpAuthentication,\n ContentType: \"application/json\"\n },\n \"payload\": {\n \"encode\": \"text\",\n \"property-references\": property_references\n },\n \"requestTimeout\": global.get('g_httpRequestTimeOut'),\n \"device\": device\n }\n\n ///////////////////////////////////////////////////////////\n ////// XXXXX Controller\n ////// URL to the API documentation\n ///////////////////////////////////////////////////////////\n case \"anotherController\":\n\n\n}\n\n\n\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1280, "y": 320, "wires": [ [ "0a876338e2be6dd8" ] ] }, { "id": "0a876338e2be6dd8", "type": "http request", "z": "e5621a7de149225c", "g": "58e5886e40ebacd1", "name": "HTTP REQUEST", "method": "use", "ret": "txt", "paytoqs": "ignore", "url": "", "tls": "2418490b3512393e", "persist": false, "proxy": "", "insecureHTTPParser": false, "authType": "", "senderr": false, "headers": [], "x": 1550, "y": 320, "wires": [ [ "d04fc338048162ec" ] ] }, { "id": "5e76761149f4280e", "type": "function", "z": "e5621a7de149225c", "g": "58e5886e40ebacd1", "name": "Creation results", "func": "let device = msg.device;\nconst debug = flow.get('$parent.g_debug');\n\nswitch (msg.statusCode) {\n ////////////////////////////////////////////////// \n // Case 200 : \"Success\" > Objects have been created\n //////////////////////////////////////////////////\n case 200:\n if (msg.payload.includes(\"\\\"status\\\":200\")) {\n debug(device, \"creation\", `${device.identity.deviceName} (RestAPI) : Some BACnet objects have been created`);\n }\n if (msg.payload.includes(\"Instance already exists\") || msg.payload.includes(\"Object with same name already exists\")) {\n flow.set('g_errorObjectCreation', flow.get('g_errorObjectCreation') + 1);\n node.error(`${device.identity.deviceName} : Some BACnet objects already existed`);\n }\n break;\n\n case 400:\n node.error(\"Error : Bad HTTP Request\");\n break;\n\n case 401:\n node.error(\"Error : Can't connect to controller : Authorization error\");\n break;\n\n case 500:\n node.error(\"Error : Server Error 500\");\n break;\n\n case 404:\n node.error(\"Error : 404\");\n break;\n\n\n case \"ETIMEDOUT\":\n node.error(\"Error : Can't connect to controller : TimeOut\");\n break;\n\n case \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\":\n node.error(\"Error : You forgot to enable the TLS config in your HTTP node\");\n break;\n\n}\n\n\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "// Le code ajouté ici sera exécuté une fois\n// à chaque démarrage du noeud.\nglobal.set('g_errorObjectCreation', 0);", "finalize": "", "libs": [], "x": 1780, "y": 260, "wires": [ [] ], "icon": "node-red/alert.svg" }, { "id": "d04fc338048162ec", "type": "function", "z": "e5621a7de149225c", "g": "58e5886e40ebacd1", "name": "Debug Write", "func": "let device = msg.device;\nconst debug = flow.get('$parent.g_debug');\n\ndebug(device, \"txTime\", `${device.identity.deviceName} (${device.controller.protocol}) : TX time = ${Date.now() - device.transmitTime} ms`); \n\nreturn msg;", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1770, "y": 320, "wires": [ [ "d7fb76702f1ebdab" ] ] }, { "id": "0ad4d2e61e8cd612", "type": "change", "z": "e5621a7de149225c", "name": "Read/Write in process", "rules": [ { "t": "delete", "p": "payload", "pt": "msg" }, { "t": "set", "p": "payload.text", "pt": "msg", "to": "$env('dataDirection') & \" Read/Write in process...\"", "tot": "jsonata" }, { "t": "set", "p": "payload.fill", "pt": "msg", "to": "yellow", "tot": "str" }, { "t": "set", "p": "payload.shape", "pt": "msg", "to": "ring", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 460, "y": 600, "wires": [ [ "c5511cd0867d06e2" ] ] }, { "id": "a3a2696eda710a5e", "type": "change", "z": "e5621a7de149225c", "name": "Read/Write complete", "rules": [ { "t": "delete", "p": "payload", "pt": "msg" }, { "t": "set", "p": "payload.text", "pt": "msg", "to": "$env('dataDirection') & \" Read/Write complete\"", "tot": "jsonata" }, { "t": "set", "p": "payload.fill", "pt": "msg", "to": "green", "tot": "str" }, { "t": "set", "p": "payload.shape", "pt": "msg", "to": "dot", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 460, "y": 560, "wires": [ [ "c5511cd0867d06e2" ] ] }, { "id": "ed3e8b9be7a13e70", "type": "change", "z": "e5621a7de149225c", "name": "Object creation complete", "rules": [ { "t": "delete", "p": "payload", "pt": "msg" }, { "t": "set", "p": "payload.text", "pt": "msg", "to": "$env('dataDirection') & \" Object creation complete\"", "tot": "jsonata" }, { "t": "set", "p": "payload.fill", "pt": "msg", "to": "green", "tot": "str" }, { "t": "set", "p": "payload.shape", "pt": "msg", "to": "dot", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 470, "y": 640, "wires": [ [ "c5511cd0867d06e2" ] ] }, { "id": "e3be43afc71d8df9", "type": "change", "z": "e5621a7de149225c", "name": "Object creation in process", "rules": [ { "t": "delete", "p": "payload", "pt": "msg" }, { "t": "set", "p": "payload.text", "pt": "msg", "to": "$env('dataDirection') & \" Object creation in process...\"", "tot": "jsonata" }, { "t": "set", "p": "payload.fill", "pt": "msg", "to": "yellow", "tot": "str" }, { "t": "set", "p": "payload.shape", "pt": "msg", "to": "ring", "tot": "str" } ], "action": "", "property": "", "from": "", "to": "", "reg": false, "x": 470, "y": 520, "wires": [ [ "c5511cd0867d06e2" ] ] }, { "id": "d7fb76702f1ebdab", "type": "link out", "z": "e5621a7de149225c", "g": "58e5886e40ebacd1", "name": "link out Object creation complete", "mode": "link", "links": [ "9fdaa35d3e506094" ], "x": 1875, "y": 320, "wires": [] }, { "id": "9fdaa35d3e506094", "type": "link in", "z": "e5621a7de149225c", "name": "link in Object creation complete", "links": [ "d7fb76702f1ebdab" ], "x": 295, "y": 640, "wires": [ [ "ed3e8b9be7a13e70" ] ] }, { "id": "408508b0ed73706f", "type": "link in", "z": "e5621a7de149225c", "name": "link in Read/Write in progress", "links": [ "7d4514aea698cd4b" ], "x": 295, "y": 600, "wires": [ [ "0ad4d2e61e8cd612" ] ] }, { "id": "452d87a8fe8e5e5b", "type": "link in", "z": "e5621a7de149225c", "name": "link in Read/Write complete", "links": [ "a1b40b26fe6dd41f" ], "x": 295, "y": 560, "wires": [ [ "a3a2696eda710a5e" ] ] }, { "id": "b6703631b9a583d8", "type": "link in", "z": "e5621a7de149225c", "name": "link in object creation in process", "links": [ "780a20b5b444c3d7" ], "x": 295, "y": 520, "wires": [ [ "e3be43afc71d8df9" ] ] }, { "id": "780a20b5b444c3d7", "type": "link out", "z": "e5621a7de149225c", "name": "link out object creation", "mode": "link", "links": [ "b6703631b9a583d8" ], "x": 1015, "y": 260, "wires": [] }, { "id": "7d4514aea698cd4b", "type": "link out", "z": "e5621a7de149225c", "name": "link out Read/Write in progress", "mode": "link", "links": [ "408508b0ed73706f" ], "x": 535, "y": 420, "wires": [] }, { "id": "6c31111be68ac559", "type": "function", "z": "e5621a7de149225c", "name": "Store Downlink object", "func": "/////////////////////////////////////////////////////////////////////\n///////////////// Store Downlink Objects //////////////\n/////////////////////////////////////////////////////////////////////\n/* This function stores the downlink data from the controller */\n\nlet device = msg.device;\nlet bacnetObjects = device.bacnet.objects;\n\n// For InfluxDB support\ndevice.influxdb.source = \"downlink\";\n\nswitch (device.controller.model) {\n case \"distechControlsV2\":\n let donwlinkObjects = msg.payload;\n\n for (let i = 0; i < donwlinkObjects.results.length; i++) {\n Object.values(bacnetObjects).forEach(obj => {\n if (donwlinkObjects.results[i].type == obj.objectType && donwlinkObjects.results[i].instance == obj.instanceNum) {\n if (obj.objectType == \"analogValue\") obj.value = Number(donwlinkObjects.results[i].value);\n if (obj.objectType == \"binaryValue\") obj.value = donwlinkObjects.results[i].value;\n }\n });\n }\n\n return {\n device: device\n };\n\n\n\n ///////////////////////////////////////////////////////////\n ////// XXXXX Controller\n ////// URL to the API documentation\n ///////////////////////////////////////////////////////////\n case \"anotherController\":\n\n}\n", "outputs": 1, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1320, "y": 420, "wires": [ [ "f8a6a26c3a4274bf" ] ] }, { "id": "a1b40b26fe6dd41f", "type": "link out", "z": "e5621a7de149225c", "name": "link out Read/Write complete", "mode": "link", "links": [ "452d87a8fe8e5e5b" ], "x": 1535, "y": 500, "wires": [] }, { "id": "8118aa09800ae8ee", "type": "switch", "z": "e5621a7de149225c", "name": "", "property": "dataDirection", "propertyType": "env", "rules": [ { "t": "eq", "v": "downlink", "vt": "str" }, { "t": "eq", "v": "uplink", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 2, "x": 1175, "y": 460, "wires": [ [ "6c31111be68ac559" ], [ "f8a6a26c3a4274bf" ] ], "outputLabels": [ "downlink", "uplink" ], "l": false }, { "id": "580fb662e924a681", "type": "switch", "z": "e5621a7de149225c", "name": "", "property": "device.controller.protocol", "propertyType": "msg", "rules": [ { "t": "eq", "v": "RestAPIBacnet", "vt": "str" } ], "checkall": "true", "repair": false, "outputs": 1, "x": 450, "y": 420, "wires": [ [ "7d4514aea698cd4b" ] ] }, { "id": "adb0c33fcb917696", "type": "mqtt out", "z": "d5185b34c44ae683", "g": "90c82936d1f21eda", "name": "MQTT Publisher", "topic": "", "qos": "", "retain": "", "respTopic": "", "contentType": "", "userProps": "", "correl": "", "expiry": "", "broker": "d6dd3de173c23998", "x": 1240, "y": 300, "wires": [] }, { "id": "4fbd348dfae7d9ce", "type": "function", "z": "d5185b34c44ae683", "g": "90c82936d1f21eda", "name": "prepare downlink", "func": "///////////////////////////////////////////////////////////\n////// This part is device dependant\n////// The configuration depends on the downlink strategy\n///////////////////////////////////////////////////////////\n\nlet device = msg.device;\n\nvar staticDownlinkObjects = device.lorawan.defaultValuesForDownlink ?? null ;\n\nlet bacnetObjects = device.bacnet.objects;\nconst debug = flow.get(\"g_debug\");\nlet downlinkLowPriorityObject = 0, downlinkHighPriorityObject = 0; \nlet previousValues = flow.get(\"g_previousValues\");\nlet previousBacnetObject = previousValues[device.identity.deviceName].bacnet.objects;\nlet payload={};\n\nfunction downlinkPayloadCreation(downlinkObjectToSend) {\n //Creation of the downlink payload\n for (let obj in bacnetObjects){\n if (bacnetObjects[obj].dataDirection === \"downlink\" && bacnetObjects[obj].downlinkPort == bacnetObjects[downlinkObjectToSend].downlinkPort){\n let temp = \"{ \\\"\" + obj + \"\\\" : \" + JSON.stringify(bacnetObjects[obj].value) + \" }\"; \n payload = { ...payload, ...JSON.parse(temp) }\n }\n }\n // Chek if there are other values to add to the payload\n if (device.lorawan.hasOwnProperty(\"defaultValuesForDownlink\") ){\n node.warn(\"my warning\");\n if (device.lorawan.defaultValuesForDownlink.hasOwnProperty(\"fPort_\" + bacnetObjects[downlinkObjectToSend].downlinkPort)){\n for (let obj in staticDownlinkObjects[\"fPort_\"+ bacnetObjects[downlinkObjectToSend].downlinkPort]){\n let temp = \"{ \\\"\" + obj + \"\\\" : \" + JSON.stringify(staticDownlinkObjects[\"fPort_\"+ bacnetObjects[downlinkObjectToSend].downlinkPort][obj]) + \" }\"; \n payload = { ...payload, ...JSON.parse(temp) }\n \n }\n }\n }\n msg.device.lorawan.downlinkPort = bacnetObjects[downlinkObjectToSend].downlinkPort\n \n}\n\nfor (let object in bacnetObjects) {\n\n if (bacnetObjects[object].dataDirection === \"downlink\") {\n \n switch (bacnetObjects[object].downlinkPortPriority) {\n case \"high\":\n switch (bacnetObjects[object].downlinkStrategy) {\n case \"onChangeOfThisValue\":\n if (bacnetObjects[object].value != previousBacnetObject[object].value) {\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"Downlink high priority COV\" });\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : Previous value ${previousBacnetObject[object].value} != ${object} ${bacnetObjects[object].value}`);\n //Creation of the downlink payload\n downlinkPayloadCreation(object)\n }\n break;\n case \"onChangeOfThisValueWithinRange\":\n if (bacnetObjects[object].value != previousBacnetObject[object].value && bacnetObjects[object].value <= bacnetObjects[object].range[1] && bacnetObjects[object].value >= bacnetObjects[object].range[0]) {\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"Downlink high priority COVWR\" });\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : Previous value ${previousBacnetObject[object].value} != ${object} ${bacnetObjects[object].value}`);\n //Creation of the downlink payload\n downlinkPayloadCreation(object)\n }\n break;\n case \"compareToUplinkObjectWithinRange\":\n if (bacnetObjects[object].value != bacnetObjects[bacnetObjects[object].uplinkToCompareWith].value && bacnetObjects[object].value <= bacnetObjects[object].range[1] && bacnetObjects[object].value >= bacnetObjects[object].range[0]) {\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"Downlink high priority CUVWR\" });\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : ${bacnetObjects[object].uplinkToCompareWith} ${bacnetObjects[bacnetObjects[object].uplinkToCompareWith].value} != ${object} ${bacnetObjects[object].value}`);\n //Creation of the downlink payload\n downlinkPayloadCreation(object)\n }\n break;\n case \"compareToUplinkObject\":\n if (bacnetObjects[object].value != bacnetObjects[bacnetObjects[object].uplinkToCompareWith].value) {\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"Downlink high priority CUV\" });\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : ${bacnetObjects[object].uplinkToCompareWith} ${bacnetObjects[bacnetObjects[object].uplinkToCompareWith].value} != ${object} ${bacnetObjects[object].value}`);\n //Creation of the downlink payload\n downlinkPayloadCreation(object)\n }\n break;\n default:\n \n }\n \n break;\n case \"low\":\n //In case of low priority downlink the object name is kept till the end of the for loop \n // to be sur that there is not any high priority downlink to send \n switch (bacnetObjects[object].downlinkStrategy) {\n case \"onChangeOfThisValue\":\n if (bacnetObjects[object].value != previousBacnetObject[object].value) {\n downlinkLowPriorityObject = object;\n }\n break;\n case \"onChangeOfThisValueWithinRange\":\n if (bacnetObjects[object].value != previousBacnetObject[object].value && bacnetObjects[object].value <= bacnetObjects[object].range[1] && bacnetObjects[object].value >= bacnetObjects[object].range[0]) {\n downlinkLowPriorityObject = object;\n }\n break;\n case \"compareToUplinkObjectWithinRange\":\n if (bacnetObjects[object].value != bacnetObjects[bacnetObjects[object].uplinkToCompareWith]?.value && bacnetObjects[object].value <= bacnetObjects[object].range[1] && bacnetObjects[object].value >= bacnetObjects[object].range[0]) {\n downlinkLowPriorityObject = object;\n }\n break;\n case \"compareToUplinkObject\":\n if (bacnetObjects[object].value != bacnetObjects[bacnetObjects[object].uplinkToCompareWith].value) {\n downlinkLowPriorityObject = object;\n }\n break;\n default:\n \n }\n break;\n default:\n\n }\n }\n if (Object.keys(payload).length !== 0){\n break;\n }\n}\nif (downlinkLowPriorityObject != 0 && Object.keys(payload).length === 0) {\n node.status({ fill: \"yellow\", shape: \"dot\", text: \"Downlink low priority\" });\n switch (bacnetObjects[downlinkLowPriorityObject].downlinkStrategy) {\n case \"onChangeOfThisValue\":\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : Previous value ${previousBacnetObject[downlinkLowPriorityObject].value} != ${downlinkLowPriorityObject} ${bacnetObjects[downlinkLowPriorityObject].value}`);\n break;\n case \"onChangeOfThisValueWithinRange\":\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : Previous value ${previousBacnetObject[downlinkLowPriorityObject].value} != ${downlinkLowPriorityObject} ${bacnetObjects[downlinkLowPriorityObject].value}`);\n break;\n case \"compareToUplinkObjectWithinRange\":\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : ${bacnetObjects[downlinkLowPriorityObject].uplinkToCompareWith} ${bacnetObjects[bacnetObjects[downlinkLowPriorityObject].uplinkToCompareWith].value} != ${downlinkLowPriorityObject} ${bacnetObjects[downlinkLowPriorityObject].value}`);\n break;\n case \"compareToUplinkObject\":\n debug(device, \"down\", `${device.identity.deviceName} (${device.controller.protocol}) : Downlink scheduled : ${bacnetObjects[downlinkLowPriorityObject].uplinkToCompareWith} ${bacnetObjects[bacnetObjects[downlinkLowPriorityObject].uplinkToCompareWith].value} != ${downlinkLowPriorityObject} ${bacnetObjects[downlinkLowPriorityObject].value}`);\n break;\n default:\n \n }\n //Creation of the dowlink payload\n downlinkPayloadCreation(downlinkLowPriorityObject) \n}else if (Object.keys(payload).length === 0){\n\n node.status({fill: \"green\", shape: \"dot\" ,text: \"No downlink\"});\n return null;\n}\n\n//Update the previous values\nfor (let object in payload) {\n previousBacnetObject[object].value = payload[object] ;\n}\n\n\n//Create the downlink