node-red-contrib-lorawan-bacnet-server 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of node-red-contrib-lorawan-bacnet-server might be problematic. Click here for more details.
- package/.gitattributes +2 -0
- package/CONTRIBUTING.md +64 -0
- package/LICENSE +21 -0
- package/README.md +111 -0
- package/examples/BACnet-Server.json +401 -0
- package/examples/LoRaBAC.json +1152 -0
- package/images/BACnetObjectListExample.png +0 -0
- package/images/controllerSetpointExample3.png +0 -0
- package/images/deviceListExample.png +0 -0
- package/images/deviceListExample3.png +0 -0
- package/images/lorabac.png +0 -0
- package/images/objectConfigurationExample2.png +0 -0
- package/images/objectListExample2.png +0 -0
- package/images/objectListExample3.png +0 -0
- package/images/valveSetpointExample3.png +0 -0
- package/images/valveTemperatureExample3.png +0 -0
- package/nodes/bacnet-point/bacnet-point.html +403 -0
- package/nodes/bacnet-point/bacnet-point.js +293 -0
- package/nodes/bacnet-server/bacnet-server.html +138 -0
- package/nodes/bacnet-server/bacnet-server.js +817 -0
- package/nodes/lorabac/lorabac.html +1588 -0
- package/nodes/lorabac/lorabac.js +652 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1588 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType("LoRaBAC", {
|
|
3
|
+
category: "LoRaBAC",
|
|
4
|
+
color: "#00aff0",
|
|
5
|
+
icon: "node-red/cog.svg",
|
|
6
|
+
defaults: {
|
|
7
|
+
name: { value: "LoRaBAC" },
|
|
8
|
+
// Saved/Default Configuration
|
|
9
|
+
globalConfig: {
|
|
10
|
+
value: {
|
|
11
|
+
ipAddress: "",
|
|
12
|
+
networkServer: "tts",
|
|
13
|
+
grpcApiKey: "",
|
|
14
|
+
serverAddress: "",
|
|
15
|
+
grpcPort: 8080,
|
|
16
|
+
protocol: "bacnet",
|
|
17
|
+
model: "distechControlsV2",
|
|
18
|
+
bacnetLogin: "",
|
|
19
|
+
bacnetPassword: "",
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
// Devices
|
|
23
|
+
deviceCount: { value: 1 },
|
|
24
|
+
arrayDeviceList: { value: [] },
|
|
25
|
+
deviceList: { value: {} }
|
|
26
|
+
},
|
|
27
|
+
inputs: 1,
|
|
28
|
+
outputs: 1,
|
|
29
|
+
label: function () {
|
|
30
|
+
return this.name || "LoRaBAC";
|
|
31
|
+
},
|
|
32
|
+
oneditprepare: function () {
|
|
33
|
+
var node = this;
|
|
34
|
+
|
|
35
|
+
// Inputs
|
|
36
|
+
var ipAddressInput = $("#input-ip-address");
|
|
37
|
+
var networkServerSelect = $("#select-network-server");
|
|
38
|
+
var chirpstackGrpcApiKey = $("#input-grpc-api-key");
|
|
39
|
+
var chirpstackServerAddress = $("#input-server-address");
|
|
40
|
+
var chirpstackGrpcPort = $("#input-grpc-port");
|
|
41
|
+
var protocolSelect = $("#select-protocol");
|
|
42
|
+
var modelSelect = $("#select-bacnet-api-model");
|
|
43
|
+
var bacnetLoginInput = $("#input-bacnet-login");
|
|
44
|
+
var bacnetPasswordInput = $("#input-bacnet-password");
|
|
45
|
+
|
|
46
|
+
//Containers
|
|
47
|
+
var chirpstackConfigContainer = $("#chirpstack-config");
|
|
48
|
+
var bacnetSettingsContainer = $("#bacnet-settings");
|
|
49
|
+
var deviceListContainer = $("#device-list-container");
|
|
50
|
+
|
|
51
|
+
// Buttons
|
|
52
|
+
var importFromJsonBtn = $("#button-import-from-json");
|
|
53
|
+
var addDeviceBtn = $("#button-add-device");
|
|
54
|
+
|
|
55
|
+
// Restore the configuration
|
|
56
|
+
if (node.globalConfig.ipAddress) ipAddressInput.val(node.globalConfig.ipAddress);
|
|
57
|
+
if (node.globalConfig.networkServer) networkServerSelect.val(node.globalConfig.networkServer);
|
|
58
|
+
if (node.globalConfig.grpcApiKey) chirpstackGrpcApiKey.val(node.globalConfig.grpcApiKey);
|
|
59
|
+
if (node.globalConfig.serverAddress) chirpstackServerAddress.val(node.globalConfig.serverAddress);
|
|
60
|
+
if (node.globalConfig.grpcPort) chirpstackGrpcPort.val(node.globalConfig.grpcPort);
|
|
61
|
+
if (node.globalConfig.protocol) protocolSelect.val(node.globalConfig.protocol);
|
|
62
|
+
if (node.globalConfig.model) modelSelect.val(node.globalConfig.model);
|
|
63
|
+
if (node.globalConfig.bacnetLogin) bacnetLoginInput.val(node.globalConfig.bacnetLogin);
|
|
64
|
+
if (node.globalConfig.bacnetPassword) bacnetPasswordInput.val(node.globalConfig.bacnetPassword);
|
|
65
|
+
|
|
66
|
+
// Toggle configurations inputs visibility
|
|
67
|
+
function toggleChirpstackConfig() {
|
|
68
|
+
if (networkServerSelect.val() === "chirpstack") {
|
|
69
|
+
chirpstackConfigContainer.show();
|
|
70
|
+
} else {
|
|
71
|
+
chirpstackConfigContainer.hide();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function toggleBacnetSettings() {
|
|
76
|
+
if (protocolSelect.val() === "restAPIBacnet") {
|
|
77
|
+
bacnetSettingsContainer.show();
|
|
78
|
+
} else {
|
|
79
|
+
bacnetSettingsContainer.hide();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
networkServerSelect.change(function () {
|
|
84
|
+
node.networkServer = networkServerSelect.val();
|
|
85
|
+
toggleChirpstackConfig();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
protocolSelect.change(function () {
|
|
89
|
+
node.protocol = protocolSelect.val();
|
|
90
|
+
toggleBacnetSettings();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Tool tips setup
|
|
94
|
+
|
|
95
|
+
RED.events.on("editor:open", function () {
|
|
96
|
+
setTimeout(setupTooltips, 200);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
RED.events.on("tray:open", function () {
|
|
100
|
+
setTimeout(setupTooltips, 200);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Buttons setup
|
|
104
|
+
|
|
105
|
+
addDeviceBtn.click(function () {
|
|
106
|
+
addNewDevice(node);
|
|
107
|
+
updateDeviceList(node);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
importFromJsonBtn.on("click", function () {
|
|
111
|
+
saveGlobalConfig(node);
|
|
112
|
+
formatArrayToJSON(node);
|
|
113
|
+
showImportJsonTray(node);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Init
|
|
117
|
+
toggleChirpstackConfig();
|
|
118
|
+
toggleBacnetSettings();
|
|
119
|
+
setupTooltips();
|
|
120
|
+
updateDeviceList(node);
|
|
121
|
+
},
|
|
122
|
+
// ==================================================
|
|
123
|
+
// =============== MAIN TRAY SAVE ===================
|
|
124
|
+
// ==================================================
|
|
125
|
+
|
|
126
|
+
oneditsave: function () {
|
|
127
|
+
formatArrayToJSON(this);
|
|
128
|
+
console.log(this.globalConfig);
|
|
129
|
+
console.log(this.deviceList);
|
|
130
|
+
let status = verifyDeviceList(this.deviceList);
|
|
131
|
+
if (!status.ok) {
|
|
132
|
+
logErrorStatus(status)
|
|
133
|
+
} else {
|
|
134
|
+
this.changed = true;
|
|
135
|
+
RED.nodes.dirty(this.id);
|
|
136
|
+
RED.notify("Device list saved", "success");
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Save the global configuration into the node
|
|
143
|
+
*/
|
|
144
|
+
function saveGlobalConfig(node) {
|
|
145
|
+
node.globalConfig.ipAddress = $("#input-ip-address").val();
|
|
146
|
+
node.globalConfig.networkServer = $("#select-network-server").val();
|
|
147
|
+
node.globalConfig.grpcApiKey = $("#input-grpc-api-key").val();
|
|
148
|
+
node.globalConfig.serverAddress = $("#input-server-address").val();
|
|
149
|
+
node.globalConfig.grpcPort = $("#input-grpc-port").val();
|
|
150
|
+
node.globalConfig.protocol = $("#select-protocol").val();
|
|
151
|
+
node.globalConfig.bacnetLogin = $("#input-bacnet-login").val();
|
|
152
|
+
node.globalConfig.bacnetPassword = $("#input-bacnet-password").val();
|
|
153
|
+
node.globalConfig.model = $("#select-bacnet-api-model").val();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function enforceNumberInRange($input, options) {
|
|
157
|
+
const {
|
|
158
|
+
min = 0,
|
|
159
|
+
max = Infinity,
|
|
160
|
+
name = "Value",
|
|
161
|
+
allowFloat = false
|
|
162
|
+
} = options;
|
|
163
|
+
|
|
164
|
+
$input.blur(function () {
|
|
165
|
+
let val = allowFloat ? parseFloat($input.val()) : parseInt($input.val(), 10);
|
|
166
|
+
|
|
167
|
+
if (isNaN(val) || val < min || val > max) {
|
|
168
|
+
RED.notify(`${name} must be a ${allowFloat ? 'number' : 'whole number'} between ${min} and ${max}`, "error");
|
|
169
|
+
val = Math.max(min, Math.min(val || 0, max)); // Clamping
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
$input.val(allowFloat ? Number(val) : parseInt(val, 10));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ==================================================
|
|
177
|
+
// =============== DEVICE FUNCTIONS =================
|
|
178
|
+
// ==================================================
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Create a new device based on the base device
|
|
182
|
+
* Filled with default values if not found in base device / no base device
|
|
183
|
+
* Base device is a device from a "formated" JSON device list
|
|
184
|
+
*/
|
|
185
|
+
function newDevice(node, baseDeviceName = null, baseDevice = {}) {
|
|
186
|
+
let device = {
|
|
187
|
+
deviceName: baseDeviceName || `device-${node.deviceCount}`,
|
|
188
|
+
maxDevNum: baseDevice.identity?.maxDevNum ?? 10,
|
|
189
|
+
debugAll: baseDevice.controller?.debug?.includes("all") ?? true,
|
|
190
|
+
debugUp: baseDevice.controller?.debug?.includes("up") ?? false,
|
|
191
|
+
debugDown: baseDevice.controller?.debug?.includes("down") ?? false,
|
|
192
|
+
debugCreation: baseDevice.controller?.debug?.includes("creation") ?? false,
|
|
193
|
+
debugTxTime: baseDevice.controller?.debug?.includes("creation") ?? false,
|
|
194
|
+
flushDownlinkQueue: baseDevice.lorawan?.flushDownlinkQueue ?? false,
|
|
195
|
+
class: baseDevice.lorawan?.class ?? "A",
|
|
196
|
+
pId: baseDevice.actility?.pId ?? "",
|
|
197
|
+
mId: baseDevice.actility?.mId ?? "",
|
|
198
|
+
ver: baseDevice.actility?.ver ?? "",
|
|
199
|
+
offsetAV: baseDevice.bacnet?.offsetAV ?? 0,
|
|
200
|
+
offsetBV: baseDevice.bacnet?.offsetBV ?? 0,
|
|
201
|
+
instanceRangeAV: baseDevice.bacnet?.instanceRangeAV ?? 10,
|
|
202
|
+
instanceRangeBV: baseDevice.bacnet?.instanceRangeBV ?? 10,
|
|
203
|
+
objectsCount: 1,
|
|
204
|
+
objects: [],
|
|
205
|
+
};
|
|
206
|
+
return device;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function addNewDevice(node) {
|
|
210
|
+
let device = newDevice(node);
|
|
211
|
+
node.arrayDeviceList.push(device);
|
|
212
|
+
node.deviceCount++;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Return if the device has at least one downlink object
|
|
217
|
+
*/
|
|
218
|
+
function hasDownlinkObject(device) {
|
|
219
|
+
return device.objects.some(function (object) {
|
|
220
|
+
return object.dataDirection === "downlink";
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Update device list on the main tray
|
|
226
|
+
* and add event listeners to the edit and delete buttons
|
|
227
|
+
*/
|
|
228
|
+
function updateDeviceList(node) {
|
|
229
|
+
const deviceListContainer = $("#device-list-container");
|
|
230
|
+
deviceListContainer.empty();
|
|
231
|
+
node.arrayDeviceList.forEach(function (device, index) {
|
|
232
|
+
const row = $("<div>").addClass("form-row").appendTo(deviceListContainer);
|
|
233
|
+
|
|
234
|
+
// Device name input
|
|
235
|
+
const deviceNameInput = $("<input>")
|
|
236
|
+
.attr("type", "text")
|
|
237
|
+
.val(device.deviceName)
|
|
238
|
+
.addClass("node-input-list-item")
|
|
239
|
+
.appendTo(row);
|
|
240
|
+
|
|
241
|
+
// Update device name input when editing
|
|
242
|
+
deviceNameInput.on("input", function () {
|
|
243
|
+
device.deviceName = $(this).val();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// Edit button
|
|
247
|
+
$("<button>")
|
|
248
|
+
.html(`<i class="fa fa-pencil"></i>`)
|
|
249
|
+
.attr("title", "Modifier")
|
|
250
|
+
.addClass("red-ui-button")
|
|
251
|
+
.css("margin-left", "10px")
|
|
252
|
+
.click(function () {
|
|
253
|
+
showDeviceTray(node, device);
|
|
254
|
+
})
|
|
255
|
+
.appendTo(row);
|
|
256
|
+
|
|
257
|
+
// Delete button
|
|
258
|
+
$("<button>")
|
|
259
|
+
.html(`<i class="fa fa-trash"></i>`)
|
|
260
|
+
.attr("title", "Supprimer")
|
|
261
|
+
.addClass("red-ui-button")
|
|
262
|
+
.css("margin-left", "10px")
|
|
263
|
+
.click(function () {
|
|
264
|
+
node.arrayDeviceList.splice(index, 1);
|
|
265
|
+
updateDeviceList(node);
|
|
266
|
+
})
|
|
267
|
+
.appendTo(row);
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ==================================================
|
|
272
|
+
// =============== DEVICE TRAY ======================
|
|
273
|
+
// ==================================================
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Open a new tray to configure a device
|
|
277
|
+
*/
|
|
278
|
+
function showDeviceTray(node, device) {
|
|
279
|
+
saveGlobalConfig(node);
|
|
280
|
+
// Input variables
|
|
281
|
+
let selectClass,
|
|
282
|
+
inputMaxDevNum,
|
|
283
|
+
checkboxDebugAll,
|
|
284
|
+
checkboxDebugUp,
|
|
285
|
+
checkboxDebugDown,
|
|
286
|
+
checkboxDebugCreation,
|
|
287
|
+
checkboxDebugTxTime,
|
|
288
|
+
selectFlushDownlinkQueue,
|
|
289
|
+
inputOffsetAV,
|
|
290
|
+
inputOffsetBV,
|
|
291
|
+
inputInstanceRangeAV,
|
|
292
|
+
inputInstanceRangeBV,
|
|
293
|
+
inputPId,
|
|
294
|
+
inputMId,
|
|
295
|
+
inputVer;
|
|
296
|
+
|
|
297
|
+
RED.tray.show({
|
|
298
|
+
title: "Configure device",
|
|
299
|
+
width: 400,
|
|
300
|
+
open: function (tray) {
|
|
301
|
+
// Fill with HTML template
|
|
302
|
+
let trayBody = tray.find(".red-ui-tray-body");
|
|
303
|
+
trayBody.html($("#device-config-template").html());
|
|
304
|
+
$("#device-config-title").text(`"${device.deviceName}" device configuration`); // Set title
|
|
305
|
+
$("#instance-num-settings-title").text(`Instance Num settings for "${device.deviceName}"`);
|
|
306
|
+
|
|
307
|
+
// Init input variables
|
|
308
|
+
selectClass = $("#select-class");
|
|
309
|
+
inputMaxDevNum = $("#input-max-dev-num");
|
|
310
|
+
checkboxDebugAll = $("#debug-all");
|
|
311
|
+
checkboxDebugUp = $("#debug-up");
|
|
312
|
+
checkboxDebugDown = $("#debug-down");
|
|
313
|
+
checkboxDebugCreation = $("#debug-creation");
|
|
314
|
+
checkboxDebugTxTime = $("#debug-txTime");
|
|
315
|
+
selectFlushDownlinkQueue = $("#select-flush-downlink-queue");
|
|
316
|
+
inputOffsetAV = $("#input-offset-av");
|
|
317
|
+
inputOffsetBV = $("#input-offset-bv");
|
|
318
|
+
inputInstanceRangeAV = $("#input-instance-range-av");
|
|
319
|
+
inputInstanceRangeBV = $("#input-instance-range-bv");
|
|
320
|
+
inputPId = $("#input-pId-device");
|
|
321
|
+
inputMId = $("#input-mId-device");
|
|
322
|
+
inputVer = $("#input-ver-device");
|
|
323
|
+
|
|
324
|
+
// Fill inputs
|
|
325
|
+
checkboxDebugAll.prop("checked", device.debugAll);
|
|
326
|
+
checkboxDebugUp.prop("checked", device.debugUp);
|
|
327
|
+
checkboxDebugDown.prop("checked", device.debugDown);
|
|
328
|
+
checkboxDebugCreation.prop("checked", device.debugCreation);
|
|
329
|
+
checkboxDebugTxTime.prop("checked", device.debugTxTime);
|
|
330
|
+
|
|
331
|
+
selectClass.val(device.class);
|
|
332
|
+
inputMaxDevNum.val(device.maxDevNum);
|
|
333
|
+
selectFlushDownlinkQueue.val(String(device.flushDownlinkQueue)); // To string conversion since it's a select
|
|
334
|
+
inputOffsetAV.val(device.offsetAV);
|
|
335
|
+
inputOffsetBV.val(device.offsetBV);
|
|
336
|
+
inputInstanceRangeAV.val(device.instanceRangeAV);
|
|
337
|
+
inputInstanceRangeBV.val(device.instanceRangeBV);
|
|
338
|
+
inputPId.val(device.pId);
|
|
339
|
+
inputMId.val(device.mId);
|
|
340
|
+
inputVer.val(device.ver);
|
|
341
|
+
|
|
342
|
+
// Toggle functions
|
|
343
|
+
function toggleDebugCheckboxes() {
|
|
344
|
+
if (checkboxDebugAll.prop("checked")) {
|
|
345
|
+
checkboxDebugUp.prop("disabled", true);
|
|
346
|
+
checkboxDebugDown.prop("disabled", true);
|
|
347
|
+
checkboxDebugCreation.prop("disabled", true);
|
|
348
|
+
checkboxDebugTxTime.prop("disabled", true);
|
|
349
|
+
} else {
|
|
350
|
+
checkboxDebugUp.prop("disabled", false);
|
|
351
|
+
checkboxDebugDown.prop("disabled", false);
|
|
352
|
+
checkboxDebugCreation.prop("disabled", false);
|
|
353
|
+
checkboxDebugTxTime.prop("disabled", false);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
checkboxDebugAll.change(toggleDebugCheckboxes)
|
|
357
|
+
|
|
358
|
+
enforceNumberInRange(inputMaxDevNum, {
|
|
359
|
+
min: 1,
|
|
360
|
+
max: Infinity,
|
|
361
|
+
name: "maxDevNum",
|
|
362
|
+
allowFloat: false
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
enforceNumberInRange(inputOffsetAV, {
|
|
366
|
+
min: 0,
|
|
367
|
+
max: Infinity,
|
|
368
|
+
name: "offsetAV",
|
|
369
|
+
allowFloat: false
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
enforceNumberInRange(inputOffsetBV, {
|
|
373
|
+
min: 0,
|
|
374
|
+
max: Infinity,
|
|
375
|
+
name: "offsetBV",
|
|
376
|
+
allowFloat: false
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
enforceNumberInRange(inputInstanceRangeAV, {
|
|
380
|
+
min: 0,
|
|
381
|
+
max: Infinity,
|
|
382
|
+
name: "instanceRangeAV",
|
|
383
|
+
allowFloat: false
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
enforceNumberInRange(inputInstanceRangeBV, {
|
|
387
|
+
min: 0,
|
|
388
|
+
max: Infinity,
|
|
389
|
+
name: "instanceRangeBV",
|
|
390
|
+
allowFloat: false
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Save dynamically actility settings
|
|
394
|
+
inputPId.blur(function () {
|
|
395
|
+
device.pId = inputPId.val();
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
inputMId.blur(function () {
|
|
399
|
+
device.mId = inputMId.val();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
inputVer.blur(function () {
|
|
403
|
+
device.ver = inputVer.val();
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
let addObjectBtn = $("#add-object-btn");
|
|
407
|
+
addObjectBtn.click(function () {
|
|
408
|
+
addNewObject(device);
|
|
409
|
+
updateObjectList(node, device);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
// Init
|
|
413
|
+
toggleDebugCheckboxes();
|
|
414
|
+
toggleActilityContainer(node, device);
|
|
415
|
+
updateObjectList(node, device);
|
|
416
|
+
},
|
|
417
|
+
buttons: [
|
|
418
|
+
{
|
|
419
|
+
text: "Cancel",
|
|
420
|
+
click: function () {
|
|
421
|
+
RED.tray.close();
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
text: "Save",
|
|
426
|
+
class: "primary",
|
|
427
|
+
click: function () {
|
|
428
|
+
device.debugAll = checkboxDebugAll.prop("checked");
|
|
429
|
+
device.debugUp = checkboxDebugUp.prop("checked");
|
|
430
|
+
device.debugDown = checkboxDebugDown.prop("checked");
|
|
431
|
+
device.debugCreation = checkboxDebugCreation.prop("checked");
|
|
432
|
+
device.debugTxTime = checkboxDebugTxTime.prop("checked");
|
|
433
|
+
device.class = selectClass.val();
|
|
434
|
+
device.maxDevNum = Number(inputMaxDevNum.val());
|
|
435
|
+
device.flushDownlinkQueue = selectFlushDownlinkQueue.val() === "true"; // Convert to boolean based on string
|
|
436
|
+
device.offsetAV = Number(inputOffsetAV.val());
|
|
437
|
+
device.offsetBV = Number(inputOffsetBV.val());
|
|
438
|
+
device.instanceRangeAV = Number(inputInstanceRangeAV.val());
|
|
439
|
+
device.instanceRangeBV = Number(inputInstanceRangeBV.val());
|
|
440
|
+
|
|
441
|
+
RED.tray.close();
|
|
442
|
+
//Timeout to allow the tray to close before updating the list
|
|
443
|
+
setTimeout(() => {
|
|
444
|
+
updateDeviceList(node);
|
|
445
|
+
}, 500);
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
]
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// ==================================================
|
|
453
|
+
// =============== OBJECTS ==========================
|
|
454
|
+
// ==================================================
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Create a new object for a device based on the base object
|
|
458
|
+
* Filled with default values if not found in base object / no base object
|
|
459
|
+
* base object is an object from a "formated" JSON device list
|
|
460
|
+
*/
|
|
461
|
+
function newObject(device, baseObjectName = null, baseObject = {}) {
|
|
462
|
+
let object = {
|
|
463
|
+
objectName: baseObjectName || `object-${device.objectsCount}`,
|
|
464
|
+
lorawanPayloadName: baseObject.lorawanPayloadName ?? "payload-name",
|
|
465
|
+
dataDirection: baseObject.dataDirection ?? "uplink",
|
|
466
|
+
downlinkPort: baseObject.downlinkPort ?? 30,
|
|
467
|
+
assignementMode: baseObject.assignementMode ?? "auto",
|
|
468
|
+
downlinkStrategy: baseObject.downlinkStrategy ?? "compareToUplinkObject",
|
|
469
|
+
instanceNum: baseObject.instanceNum ?? 0,
|
|
470
|
+
downlinkPortPriority: baseObject.downlinkPortPriority ?? "low",
|
|
471
|
+
objectType: baseObject.objectType ?? "analogValue",
|
|
472
|
+
uplinkToCompareWith: baseObject.uplinkToCompareWith ?? "",
|
|
473
|
+
rangeMin: baseObject.range?.[0] ?? 0,
|
|
474
|
+
rangeMax: baseObject.range?.[1] ?? 100,
|
|
475
|
+
binaryValue: baseObject.value ?? 0,
|
|
476
|
+
analogValue: baseObject.value ?? 0
|
|
477
|
+
};
|
|
478
|
+
return object
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Add a new object it to the list of objects of the device
|
|
483
|
+
*/
|
|
484
|
+
function addNewObject(device) {
|
|
485
|
+
let object = newObject(device);
|
|
486
|
+
device.objectsCount++;
|
|
487
|
+
device.objects.push(object);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Update device list on the main tray
|
|
492
|
+
* and add event listeners to the edit and delete buttons
|
|
493
|
+
*/
|
|
494
|
+
function updateObjectList(node, device) {
|
|
495
|
+
const objectListContainer = $("#object-list-container");
|
|
496
|
+
objectListContainer.empty();
|
|
497
|
+
|
|
498
|
+
device.objects.forEach(function (object, index) {
|
|
499
|
+
const row = $("<div>")
|
|
500
|
+
.addClass("form-row")
|
|
501
|
+
.appendTo(objectListContainer);
|
|
502
|
+
|
|
503
|
+
// Object name input
|
|
504
|
+
const objectNameInput = $("<input>")
|
|
505
|
+
.attr({
|
|
506
|
+
type: "text",
|
|
507
|
+
value: object.objectName,
|
|
508
|
+
class: "node-input-list-item",
|
|
509
|
+
})
|
|
510
|
+
.appendTo(row);
|
|
511
|
+
|
|
512
|
+
// Update device name input when leaving input
|
|
513
|
+
objectNameInput.on("input", function () {
|
|
514
|
+
object.objectName = $(this).val();
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
// Edit button
|
|
518
|
+
$("<button>")
|
|
519
|
+
.html(`<i class="fa fa-pencil"></i>`)
|
|
520
|
+
.css("margin-left", "10px")
|
|
521
|
+
.addClass("red-ui-button")
|
|
522
|
+
.click(function () {
|
|
523
|
+
showObjectTray(node, device, object);
|
|
524
|
+
})
|
|
525
|
+
.appendTo(row);
|
|
526
|
+
|
|
527
|
+
// Delete button
|
|
528
|
+
$("<button>")
|
|
529
|
+
.css("margin-left", "10px")
|
|
530
|
+
.html(`<i class="fa fa-trash"></i>`)
|
|
531
|
+
.addClass("red-ui-button")
|
|
532
|
+
.click(function () {
|
|
533
|
+
device.objects.splice(index, 1);
|
|
534
|
+
updateObjectList(node, device);
|
|
535
|
+
})
|
|
536
|
+
.appendTo(row);
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// ==================================================
|
|
541
|
+
// =============== OBJECT TRAY ======================
|
|
542
|
+
// ==================================================
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Open a new tray to configure an object
|
|
546
|
+
*/
|
|
547
|
+
function showObjectTray(node, device, object) {
|
|
548
|
+
let inputPayloadName,
|
|
549
|
+
selectDataDirection,
|
|
550
|
+
inputDownlinkPort,
|
|
551
|
+
selectObjectType,
|
|
552
|
+
inputInstanceNum,
|
|
553
|
+
selectAssignementMode,
|
|
554
|
+
selectDownlinkStrategies,
|
|
555
|
+
inputCompareUplinkObject,
|
|
556
|
+
inputRangeMin,
|
|
557
|
+
inputRangeMax,
|
|
558
|
+
selectPriority,
|
|
559
|
+
inputAnalogValue,
|
|
560
|
+
inputBinaryValue,
|
|
561
|
+
inputPId,
|
|
562
|
+
inputMId,
|
|
563
|
+
inputVer;
|
|
564
|
+
|
|
565
|
+
RED.tray.show({
|
|
566
|
+
title: "Configure object",
|
|
567
|
+
width: 600,
|
|
568
|
+
open: function (tray) {
|
|
569
|
+
// Fill with HTML template
|
|
570
|
+
let trayBody = tray.find(".red-ui-tray-body");
|
|
571
|
+
trayBody.html($("#object-config-template").html());
|
|
572
|
+
$("#object-config-title").text(`"${object.objectName}" object configuration`);
|
|
573
|
+
|
|
574
|
+
// Init input variables
|
|
575
|
+
inputPayloadName = $("#input-lorawan-payload-name");
|
|
576
|
+
selectDataDirection = $("#select-data-direction");
|
|
577
|
+
inputDownlinkPort = $("#input-downlink-port");
|
|
578
|
+
selectObjectType = $("#select-object-type");
|
|
579
|
+
inputInstanceNum = $("#input-instance-num");
|
|
580
|
+
selectAssignementMode = $("#select-assignement-mode");
|
|
581
|
+
selectDownlinkStrategies = $("#select-downlink-strategies");
|
|
582
|
+
inputCompareUplinkObject = $("#input-compare-uplink-value");
|
|
583
|
+
inputRangeMin = $("#input-change-value-range-min");
|
|
584
|
+
inputRangeMax = $("#input-change-value-range-max");
|
|
585
|
+
selectPriority = $("#select-downlink-priority");
|
|
586
|
+
inputAnalogValue = $("#input-analog-value");
|
|
587
|
+
inputBinaryValue = $("#input-binary-value");
|
|
588
|
+
inputPId = $("#input-pId-device");
|
|
589
|
+
inputMId = $("#input-mId-device");
|
|
590
|
+
inputVer = $("#input-ver-device");
|
|
591
|
+
|
|
592
|
+
// Fill inputs
|
|
593
|
+
inputPayloadName.val(object.lorawanPayloadName);
|
|
594
|
+
selectDataDirection.val(object.dataDirection);
|
|
595
|
+
inputDownlinkPort.val(object.downlinkPort);
|
|
596
|
+
selectObjectType.val(object.objectType);
|
|
597
|
+
inputInstanceNum.val(object.instanceNum);
|
|
598
|
+
selectAssignementMode.val(object.assignementMode);
|
|
599
|
+
selectDownlinkStrategies.val(object.downlinkStrategy);
|
|
600
|
+
inputCompareUplinkObject.val(object.uplinkToCompareWith);
|
|
601
|
+
inputRangeMin.val(object.rangeMin);
|
|
602
|
+
inputRangeMax.val(object.rangeMax);
|
|
603
|
+
selectPriority.val(object.downlinkPortPriority);
|
|
604
|
+
inputAnalogValue.val(object.analogValue);
|
|
605
|
+
inputBinaryValue.val(object.binaryValue);
|
|
606
|
+
|
|
607
|
+
inputPId.val(device.pId);
|
|
608
|
+
inputMId.val(device.mId);
|
|
609
|
+
inputVer.val(device.ver);
|
|
610
|
+
|
|
611
|
+
enforceNumberInRange(inputInstanceNum, {
|
|
612
|
+
min: 0,
|
|
613
|
+
max: Infinity,
|
|
614
|
+
name: "InstanceNum",
|
|
615
|
+
allowFloat: false
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
enforceNumberInRange(inputDownlinkPort, {
|
|
619
|
+
min: 0,
|
|
620
|
+
max: Infinity,
|
|
621
|
+
name: "DownlinkPort",
|
|
622
|
+
allowFloat: false
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
// Containers
|
|
628
|
+
let spanInstanceNum = $("#span-instance-num");
|
|
629
|
+
|
|
630
|
+
let analogContainer = $("#analog-value-container");
|
|
631
|
+
let binaryContainer = $("#binary-value-container");
|
|
632
|
+
|
|
633
|
+
let downlinkConfigurationContainer = $("#downlink-configuration-container");
|
|
634
|
+
let actilityConfigurationContainer = $("#actility-configuration-object");
|
|
635
|
+
let uplinkObjectContainer = $("#uplink-object-container");
|
|
636
|
+
let valueRangeContainer = $("#value-range-container");
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
// Toggles
|
|
640
|
+
function toggleInstanceNumText() {
|
|
641
|
+
if (selectAssignementMode.val() === "manual") {
|
|
642
|
+
spanInstanceNum.text("Instance Num")
|
|
643
|
+
} else if (selectAssignementMode.val() === "auto") {
|
|
644
|
+
spanInstanceNum.text("Instance Num Offset")
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function toggleValueField() {
|
|
649
|
+
if (node.globalConfig.protocol === "bacnet") {
|
|
650
|
+
analogContainer.hide();
|
|
651
|
+
binaryContainer.hide();
|
|
652
|
+
} else if (selectObjectType.val() === "analogValue") {
|
|
653
|
+
analogContainer.show();
|
|
654
|
+
binaryContainer.hide();
|
|
655
|
+
} else if (selectObjectType.val() === "binaryValue") {
|
|
656
|
+
analogContainer.hide();
|
|
657
|
+
binaryContainer.show();
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function toggleDownlinkConfiguration() {
|
|
662
|
+
if (selectDataDirection.val() === "downlink") {
|
|
663
|
+
downlinkConfigurationContainer.show();
|
|
664
|
+
if (node.globalConfig.networkServer === "actility") {
|
|
665
|
+
actilityConfigurationContainer.show();
|
|
666
|
+
} else {
|
|
667
|
+
actilityConfigurationContainer.hide();
|
|
668
|
+
}
|
|
669
|
+
} else {
|
|
670
|
+
downlinkConfigurationContainer.hide();
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function toggleDownlinkStrategiesInputs() {
|
|
675
|
+
switch (selectDownlinkStrategies.val()) {
|
|
676
|
+
case "compareToUplinkObject":
|
|
677
|
+
uplinkObjectContainer.show();
|
|
678
|
+
valueRangeContainer.hide();
|
|
679
|
+
break;
|
|
680
|
+
case "compareToUplinkObjectWithinRange":
|
|
681
|
+
uplinkObjectContainer.show();
|
|
682
|
+
valueRangeContainer.show();
|
|
683
|
+
break;
|
|
684
|
+
case "onChangeOfThisValue":
|
|
685
|
+
uplinkObjectContainer.show();
|
|
686
|
+
valueRangeContainer.hide();
|
|
687
|
+
break;
|
|
688
|
+
case "onChangeOfThisValueWithinRange":
|
|
689
|
+
uplinkObjectContainer.show();
|
|
690
|
+
valueRangeContainer.show();
|
|
691
|
+
break;
|
|
692
|
+
default:
|
|
693
|
+
uplinkObjectContainer.hide();
|
|
694
|
+
valueRangeContainer.hide();
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
selectAssignementMode.on("change", function () {
|
|
700
|
+
toggleInstanceNumText();
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
selectObjectType.on("change", function () {
|
|
704
|
+
toggleValueField();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
selectDataDirection.on("change", function () {
|
|
708
|
+
toggleDownlinkConfiguration();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
selectDownlinkStrategies.on("change", function () {
|
|
712
|
+
toggleDownlinkStrategiesInputs();
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
// Init
|
|
716
|
+
toggleInstanceNumText();
|
|
717
|
+
toggleValueField();
|
|
718
|
+
toggleDownlinkConfiguration();
|
|
719
|
+
toggleDownlinkStrategiesInputs();
|
|
720
|
+
|
|
721
|
+
},
|
|
722
|
+
buttons: [
|
|
723
|
+
{
|
|
724
|
+
text: "Cancel",
|
|
725
|
+
click: function () {
|
|
726
|
+
RED.tray.close();
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
{
|
|
730
|
+
text: "Save",
|
|
731
|
+
class: "primary",
|
|
732
|
+
click: function () {
|
|
733
|
+
object.lorawanPayloadName = inputPayloadName.val();
|
|
734
|
+
object.dataDirection = selectDataDirection.val();
|
|
735
|
+
object.downlinkPort = Number(inputDownlinkPort.val());
|
|
736
|
+
object.objectType = selectObjectType.val();
|
|
737
|
+
object.instanceNum = Number(inputInstanceNum.val());
|
|
738
|
+
object.assignementMode = selectAssignementMode.val();
|
|
739
|
+
object.downlinkStrategy = selectDownlinkStrategies.val();
|
|
740
|
+
object.downlinkPort = inputDownlinkPort.val();
|
|
741
|
+
object.downlinkPortPriority = selectPriority.val();
|
|
742
|
+
object.uplinkToCompareWith = inputCompareUplinkObject.val();
|
|
743
|
+
object.rangeMin = Number(inputRangeMin.val());
|
|
744
|
+
object.rangeMax = Number(inputRangeMax.val());
|
|
745
|
+
object.analogValue = Number(inputAnalogValue.val());
|
|
746
|
+
object.binaryValue = Number(inputBinaryValue.val());
|
|
747
|
+
|
|
748
|
+
device.pId = inputPId.val();
|
|
749
|
+
device.mId = inputMId.val();
|
|
750
|
+
device.ver = inputVer.val();
|
|
751
|
+
|
|
752
|
+
RED.tray.close();
|
|
753
|
+
//Timeout to allow the tray to close before updating the list
|
|
754
|
+
setTimeout(() => {
|
|
755
|
+
toggleActilityContainer(node, device);
|
|
756
|
+
updateObjectList(node, device);
|
|
757
|
+
}, 500);
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
]
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Show the actility container in the object tray
|
|
766
|
+
*/
|
|
767
|
+
function toggleActilityContainer(node, device) {
|
|
768
|
+
const actilityConfigurationContainer = $('#actility-configuration-device')
|
|
769
|
+
if (node.globalConfig.networkServer === "actility" && hasDownlinkObject(device)) {
|
|
770
|
+
actilityConfigurationContainer.show();
|
|
771
|
+
$("#input-pId-device").val(device.pId);
|
|
772
|
+
$("#input-mId-device").val(device.mId);
|
|
773
|
+
$("#input-ver-device").val(device.ver);
|
|
774
|
+
} else {
|
|
775
|
+
actilityConfigurationContainer.hide();
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ==================================================
|
|
780
|
+
// ============ FORMAT DEVICE LIST ==================
|
|
781
|
+
// ==================================================
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Format array device list to the final JSON
|
|
785
|
+
*/
|
|
786
|
+
function formatArrayToJSON(node) {
|
|
787
|
+
saveGlobalConfig(node);
|
|
788
|
+
node.deviceList = {};
|
|
789
|
+
node.arrayDeviceList.forEach(arrayDevice => {
|
|
790
|
+
let device = formatDevice(node, arrayDevice);
|
|
791
|
+
node.deviceList[arrayDevice.deviceName] = device;
|
|
792
|
+
})
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/**
|
|
796
|
+
* Format the device to fit in the final JSON
|
|
797
|
+
*/
|
|
798
|
+
function formatDevice(node, arrayDevice) {
|
|
799
|
+
device = {
|
|
800
|
+
identity: {
|
|
801
|
+
maxDevNum: arrayDevice.maxDevNum
|
|
802
|
+
},
|
|
803
|
+
controller: {
|
|
804
|
+
debug: [],
|
|
805
|
+
model: node.globalConfig.model,
|
|
806
|
+
protocol: node.globalConfig.protocol,
|
|
807
|
+
ipAddress: node.globalConfig.ipAddress,
|
|
808
|
+
login: node.globalConfig.bacnetLogin,
|
|
809
|
+
password: node.globalConfig.bacnetPassword,
|
|
810
|
+
httpAuthentication: arrayDevice.httpAuthentication
|
|
811
|
+
},
|
|
812
|
+
lorawan: {
|
|
813
|
+
networkServer: node.globalConfig.networkServer,
|
|
814
|
+
flushDownlinkQueue: arrayDevice.flushDownlinkQueue,
|
|
815
|
+
class: arrayDevice.class,
|
|
816
|
+
chirpstack: {
|
|
817
|
+
grpcApiKey: node.globalConfig.grpcApiKey,
|
|
818
|
+
serverAddress: node.globalConfig.serverAddress,
|
|
819
|
+
grpcPort: node.globalConfig.grpcPort
|
|
820
|
+
},
|
|
821
|
+
actility: {
|
|
822
|
+
driver: {
|
|
823
|
+
pId: arrayDevice.pId,
|
|
824
|
+
mId: arrayDevice.mId,
|
|
825
|
+
ver: arrayDevice.ver
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
bacnet: {
|
|
830
|
+
offsetAV: arrayDevice.offsetAV,
|
|
831
|
+
offsetBV: arrayDevice.offsetBV,
|
|
832
|
+
instanceRangeAV: arrayDevice.instanceRangeAV,
|
|
833
|
+
instanceRangeBV: arrayDevice.instanceRangeBV,
|
|
834
|
+
objects: {}
|
|
835
|
+
},
|
|
836
|
+
mqtt: {
|
|
837
|
+
topicDownlink: {}
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
// Conditions
|
|
842
|
+
|
|
843
|
+
// Debug
|
|
844
|
+
if (arrayDevice.debugAll) {
|
|
845
|
+
device.controller.debug = ["all"];
|
|
846
|
+
} else if (!arrayDevice.debugUp &&
|
|
847
|
+
!arrayDevice.debugDown &&
|
|
848
|
+
!arrayDevice.debugCreation &&
|
|
849
|
+
!arrayDevice.debugTxTime) {
|
|
850
|
+
device.controller.debug = []
|
|
851
|
+
} else {
|
|
852
|
+
if (arrayDevice.debugUp) device.controller.debug.push("up")
|
|
853
|
+
if (arrayDevice.debugDown) device.controller.debug.push("down")
|
|
854
|
+
if (arrayDevice.debugCreation) device.controller.debug.push("creation")
|
|
855
|
+
if (arrayDevice.debugTxTime) device.controller.debug.push("txTime")
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Remove logins and passwords if protocol is not restAPIBacnet
|
|
859
|
+
if (node.globalConfig.protocol !== "restAPIBacnet") {
|
|
860
|
+
delete device.controller.model;
|
|
861
|
+
delete device.controller.login;
|
|
862
|
+
delete device.controller.password;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Remove actility key if networkServer is not actility and no downlink objects
|
|
866
|
+
if (!(node.globalConfig.networkServer === "actility" && hasDownlinkObject(arrayDevice))) {
|
|
867
|
+
delete device.lorawan.actility;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Remove chirpstack key if networkServer is not chirpstack
|
|
871
|
+
if (node.globalConfig.networkServer !== "chirpstack") {
|
|
872
|
+
delete device.lorawan.chirpstack;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Objects
|
|
876
|
+
arrayDevice.objects.forEach(arrayObject => {
|
|
877
|
+
let object = formatObject(node, arrayObject);
|
|
878
|
+
device.bacnet.objects[arrayObject.objectName] = object;
|
|
879
|
+
})
|
|
880
|
+
return device;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function formatObject(node, arrayObject) {
|
|
884
|
+
let object = { ...arrayObject }
|
|
885
|
+
delete object.objectName;
|
|
886
|
+
object.range = [object.rangeMin, object.rangeMax];
|
|
887
|
+
delete object.rangeMin;
|
|
888
|
+
delete object.rangeMax;
|
|
889
|
+
|
|
890
|
+
if (object.dataDirection === "downlink") {
|
|
891
|
+
switch (object.downlinkStrategy) {
|
|
892
|
+
case "compareToUplinkObject":
|
|
893
|
+
delete object.range;
|
|
894
|
+
break;
|
|
895
|
+
case "compareToUplinkObjectWithinRange":
|
|
896
|
+
break;
|
|
897
|
+
// case "onChangeOfThisValue":
|
|
898
|
+
// break;
|
|
899
|
+
// case "onChangeOfThisValueWithinRange":
|
|
900
|
+
// break;
|
|
901
|
+
default:
|
|
902
|
+
delete object.uplinkToCompareWith;
|
|
903
|
+
delete object.range;
|
|
904
|
+
break;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Value based on type
|
|
908
|
+
if (object.objectType === "analogValue") {
|
|
909
|
+
object.value = object.analogValue;
|
|
910
|
+
} else if (object.objectType === "binaryValue") {
|
|
911
|
+
object.value = object.binaryValue
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
delete object.class;
|
|
915
|
+
|
|
916
|
+
// Uplink
|
|
917
|
+
} else {
|
|
918
|
+
object.value = null;
|
|
919
|
+
delete object.downlinkPortPriority;
|
|
920
|
+
delete object.downlinkPort;
|
|
921
|
+
delete object.downlinkStrategy;
|
|
922
|
+
delete object.uplinkToCompareWith;
|
|
923
|
+
delete object.range;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
delete object.binaryValue;
|
|
927
|
+
delete object.analogValue;
|
|
928
|
+
|
|
929
|
+
return object;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function unformatDeviceList(node, deviceList) {
|
|
933
|
+
let arrayDeviceList = [];
|
|
934
|
+
|
|
935
|
+
Object.entries(deviceList).forEach(([deviceName, device]) => {
|
|
936
|
+
let createdDevice = newDevice(node, deviceName, device);
|
|
937
|
+
|
|
938
|
+
Object.entries(device.bacnet?.objects ?? {}).forEach(([objectName, object]) => {
|
|
939
|
+
let createdObject = newObject(createdDevice, objectName, object);
|
|
940
|
+
createdDevice.objects.push(createdObject);
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
arrayDeviceList.push(createdDevice);
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
return arrayDeviceList;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// ==================================================
|
|
950
|
+
// =============== IMPORT JSON TRAY =================
|
|
951
|
+
// ==================================================
|
|
952
|
+
|
|
953
|
+
function showImportJsonTray(node) {
|
|
954
|
+
let jsonEditor = null;
|
|
955
|
+
let inputOverride;
|
|
956
|
+
let editorType;
|
|
957
|
+
RED.tray.show({
|
|
958
|
+
title: "Import JSON Device List",
|
|
959
|
+
width: 400,
|
|
960
|
+
open: function (tray) {
|
|
961
|
+
let trayBody = tray.find(".red-ui-tray-body");
|
|
962
|
+
trayBody.html($("#import-from-json-template").html());
|
|
963
|
+
|
|
964
|
+
inputOverride = $("#checkbox-override-device-list");
|
|
965
|
+
inputOverride.prop('checked', true);
|
|
966
|
+
|
|
967
|
+
const editorContainer = $("#json-device-list-editor")[0];
|
|
968
|
+
editorType = RED.settings.codeEditor.lib || 'monaco';
|
|
969
|
+
|
|
970
|
+
if (editorType === 'monaco') {
|
|
971
|
+
jsonEditor = monaco.editor.create(editorContainer, {
|
|
972
|
+
value: JSON.stringify(node.deviceList, null, 2),
|
|
973
|
+
language: 'json',
|
|
974
|
+
theme: 'vs',
|
|
975
|
+
automaticLayout: true,
|
|
976
|
+
fontSize: 14
|
|
977
|
+
});
|
|
978
|
+
} else {
|
|
979
|
+
jsonEditor = ace.edit(editorContainer);
|
|
980
|
+
jsonEditor.setTheme("ace/theme/textmate");
|
|
981
|
+
jsonEditor.session.setMode("ace/mode/json");
|
|
982
|
+
jsonEditor.setValue(JSON.stringify(node.deviceList, null, 2), -1);
|
|
983
|
+
}
|
|
984
|
+
},
|
|
985
|
+
close: function () {
|
|
986
|
+
if (jsonEditor) {
|
|
987
|
+
if (editorType === 'monaco') {
|
|
988
|
+
jsonEditor.dispose();
|
|
989
|
+
}
|
|
990
|
+
jsonEditor = null;
|
|
991
|
+
}
|
|
992
|
+
},
|
|
993
|
+
buttons: [
|
|
994
|
+
{
|
|
995
|
+
text: "Cancel",
|
|
996
|
+
click: function () {
|
|
997
|
+
RED.tray.close();
|
|
998
|
+
},
|
|
999
|
+
},
|
|
1000
|
+
{
|
|
1001
|
+
text: "Save",
|
|
1002
|
+
class: "primary",
|
|
1003
|
+
click: function () {
|
|
1004
|
+
RED.tray.close();
|
|
1005
|
+
|
|
1006
|
+
let editorContent;
|
|
1007
|
+
editorContent = jsonEditor.getValue();
|
|
1008
|
+
|
|
1009
|
+
let parsedDeviceList;
|
|
1010
|
+
try {
|
|
1011
|
+
parsedDeviceList = JSON.parse(editorContent);
|
|
1012
|
+
let newDevices = unformatDeviceList(node, parsedDeviceList);
|
|
1013
|
+
if (inputOverride.prop("checked")) {
|
|
1014
|
+
node.arrayDeviceList = newDevices;
|
|
1015
|
+
} else {
|
|
1016
|
+
node.arrayDeviceList = node.arrayDeviceList.concat(newDevices);
|
|
1017
|
+
}
|
|
1018
|
+
} catch (e) {
|
|
1019
|
+
console.error("Erreur de parsing JSON :", e);
|
|
1020
|
+
RED.notify("Failed to parse JSON: " + e.message, "error");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (editorType === 'monaco') {
|
|
1024
|
+
jsonEditor.dispose();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
setTimeout(() => {
|
|
1028
|
+
updateDeviceList(node);
|
|
1029
|
+
}, 500);
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
]
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
// ==================================================
|
|
1037
|
+
// =============== TOOL TIPS ========================
|
|
1038
|
+
// ==================================================
|
|
1039
|
+
|
|
1040
|
+
const tooltips = {
|
|
1041
|
+
networkServer: "LoRaWAN Network Server",
|
|
1042
|
+
protocol: "Protocol used by LoRaBAC to access your BMS.",
|
|
1043
|
+
|
|
1044
|
+
// Chirpstack Configuration
|
|
1045
|
+
grpcApiKey: "API key of your chirpstack LNS",
|
|
1046
|
+
serverAddress: "Address of your chirpstack LNS",
|
|
1047
|
+
grpcPort: "gRPC port of your chirpstack LNS",
|
|
1048
|
+
|
|
1049
|
+
// Rest API BMS Settings
|
|
1050
|
+
apiModel: "REST API of your BMS",
|
|
1051
|
+
login: "REST API login",
|
|
1052
|
+
password: "REST API password",
|
|
1053
|
+
|
|
1054
|
+
debug: "Debug level",
|
|
1055
|
+
maxDevNum: "Size of your LoRaWAN device fleet (for this type of device).",
|
|
1056
|
+
flushDownlinkQueue: "Do you want to flush the downlink queue on your LNS for each downlink.",
|
|
1057
|
+
|
|
1058
|
+
// Actility Configuration
|
|
1059
|
+
|
|
1060
|
+
// Instance Num Settings
|
|
1061
|
+
offsetAV: "Instance Num from which you want to start the storage of the Analog Value BACnet objects.",
|
|
1062
|
+
offsetBV: "Instance Num from which you want to start the storage of the Binary Value BACnet objects.",
|
|
1063
|
+
instanceRangeAV: "Number of analog value instance your want to allocate per device.",
|
|
1064
|
+
instanceRangeBV: "Number of binary value instance your want to allocate per device.",
|
|
1065
|
+
|
|
1066
|
+
lorawanPayloadName: "The LoRaWAN payload name as it is in your LoRaWAN codec.",
|
|
1067
|
+
assignementMode: " - <strong>Auto</strong> : Instance Num is automaticaly set using the following \"Instance Num Offset\".<br>- <strong>Manual</strong> : Instance Num is mannualy set by using the following \"Instance Num\".",
|
|
1068
|
+
instanceNum: "Set the instance number of the object.",
|
|
1069
|
+
dataDirection: "Is it an object related to uplink or downlink for your device?",
|
|
1070
|
+
|
|
1071
|
+
// Downlink configuration
|
|
1072
|
+
downlinkPort: "The LoRaWAN downlink Fport used for this object.",
|
|
1073
|
+
priority: "Set the downlink priority of this object.",
|
|
1074
|
+
downlinkStrategies: "Select how the application will chek if a downlink is needed.",
|
|
1075
|
+
defaultValue: "Default value when the object is created by the REST API"
|
|
1076
|
+
};
|
|
1077
|
+
|
|
1078
|
+
function setupTooltips() {
|
|
1079
|
+
$(document).on("mouseover", ".tooltip-icon", function () {
|
|
1080
|
+
const key = $(this).data("tooltip");
|
|
1081
|
+
if (!key || !tooltips[key]) return;
|
|
1082
|
+
|
|
1083
|
+
$(".lorawan-tooltip").remove();
|
|
1084
|
+
|
|
1085
|
+
const tooltip = $("<div>")
|
|
1086
|
+
.addClass("lorawan-tooltip")
|
|
1087
|
+
.html(tooltips[key]);
|
|
1088
|
+
$("body").append(tooltip);
|
|
1089
|
+
|
|
1090
|
+
const rect = this.getBoundingClientRect();
|
|
1091
|
+
tooltip.css({
|
|
1092
|
+
left: `${rect.left - 17}px`,
|
|
1093
|
+
top: `${rect.top - tooltip.outerHeight() - 10}px`,
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
const removeTooltip = () => {
|
|
1097
|
+
setTimeout(() => {
|
|
1098
|
+
if (!tooltip.is(":hover") && !$(this).is(":hover")) {
|
|
1099
|
+
tooltip.remove();
|
|
1100
|
+
}
|
|
1101
|
+
}, 200);
|
|
1102
|
+
};
|
|
1103
|
+
|
|
1104
|
+
$(this).on("mouseout", removeTooltip);
|
|
1105
|
+
tooltip.on("mouseout", removeTooltip);
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
// ==================================================
|
|
1109
|
+
// ========== DEVICE LIST VERIFICATIONS =============
|
|
1110
|
+
// ==================================================
|
|
1111
|
+
|
|
1112
|
+
function logErrorStatus(status) {
|
|
1113
|
+
RED.notify(`Error on device list : ${status.message}`, "error")
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function verifyDeviceList(deviceList) {
|
|
1117
|
+
const okayStatus = { ok: true, message: "Device list OK" };
|
|
1118
|
+
const errorStatus = (msg) => ({ ok: false, message: msg });
|
|
1119
|
+
|
|
1120
|
+
const deviceKeys = Object.keys(deviceList);
|
|
1121
|
+
|
|
1122
|
+
// Empty device list
|
|
1123
|
+
if (deviceKeys.length === 0) return okayStatus;
|
|
1124
|
+
|
|
1125
|
+
// Verify global configuration
|
|
1126
|
+
let firstDevice = deviceList[deviceKeys[0]];
|
|
1127
|
+
if (firstDevice.controller.ipAddress === "") return errorStatus("Invalid IP Address");
|
|
1128
|
+
|
|
1129
|
+
// Verify all devices/objects
|
|
1130
|
+
for (const deviceKey of deviceKeys) {
|
|
1131
|
+
const device = deviceList[deviceKey];
|
|
1132
|
+
|
|
1133
|
+
const objectKeys = Object.keys(device.bacnet?.objects || {});
|
|
1134
|
+
for (const objectKey of objectKeys) {
|
|
1135
|
+
const object = device.bacnet.objects[objectKey];
|
|
1136
|
+
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
return okayStatus;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
</script>
|
|
1144
|
+
|
|
1145
|
+
<style>
|
|
1146
|
+
/* Tooltip styles */
|
|
1147
|
+
.lorawan-tooltip {
|
|
1148
|
+
position: absolute;
|
|
1149
|
+
background-color: #162133;
|
|
1150
|
+
color: #ffffff;
|
|
1151
|
+
padding: 10px;
|
|
1152
|
+
border-radius: 5px;
|
|
1153
|
+
font-family: var(--red-ui-primary-font);
|
|
1154
|
+
font-size: 14px;
|
|
1155
|
+
max-width: 300px;
|
|
1156
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
1157
|
+
z-index: 9999;
|
|
1158
|
+
line-height: 1.5;
|
|
1159
|
+
word-wrap: break-word;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.lorawan-tooltip::after {
|
|
1163
|
+
content: "";
|
|
1164
|
+
position: absolute;
|
|
1165
|
+
bottom: -15px;
|
|
1166
|
+
left: 15px;
|
|
1167
|
+
border-width: 8px;
|
|
1168
|
+
border-style: solid;
|
|
1169
|
+
border-color: #162133 transparent transparent transparent;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.tooltip-icon {
|
|
1173
|
+
margin-left: 5px;
|
|
1174
|
+
cursor: help;
|
|
1175
|
+
color: #aaa;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.tooltip-icon:hover {
|
|
1179
|
+
color: #00aff0;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
/* Form styles */
|
|
1183
|
+
.form-row {
|
|
1184
|
+
position: relative;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.checkbox-row {
|
|
1188
|
+
margin-bottom: 5px;
|
|
1189
|
+
}
|
|
1190
|
+
</style>
|
|
1191
|
+
|
|
1192
|
+
<!-- ============================================================
|
|
1193
|
+
========== BASE NODE HTML TEMPLATE ==========
|
|
1194
|
+
============================================================ -->
|
|
1195
|
+
|
|
1196
|
+
<script type="text/html" data-template-name="LoRaBAC">
|
|
1197
|
+
<div id="device-config" class="form-horizontal" style="height: 660px; margin: 10px 20px;">
|
|
1198
|
+
|
|
1199
|
+
<div class="form-row">
|
|
1200
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
|
1201
|
+
<input type="text" id="node-input-name" placeholder="LoRaBAC Configuration">
|
|
1202
|
+
</div>
|
|
1203
|
+
|
|
1204
|
+
<div class="form-row">
|
|
1205
|
+
<h3><i class="fa fa-cog"></i> Global Configuration</h3>
|
|
1206
|
+
</div>
|
|
1207
|
+
|
|
1208
|
+
<div class="form-row">
|
|
1209
|
+
<label for="input-ip-address" style="width: 120px"><i class="fa fa-globe"></i> BMS IP Address</label>
|
|
1210
|
+
<input type="text" id="input-ip-address" style="width: 270px"/>
|
|
1211
|
+
</div>
|
|
1212
|
+
|
|
1213
|
+
<div class="form-row">
|
|
1214
|
+
<label for="select-network-server" style="width: 120px">
|
|
1215
|
+
<i class="fa fa-signal"></i> Network Server
|
|
1216
|
+
</label>
|
|
1217
|
+
<select id="select-network-server">
|
|
1218
|
+
<option value="tts">The Things Stack</option>
|
|
1219
|
+
<option value="chirpstack">Chirpstack</option>
|
|
1220
|
+
<option value="actility">Actility</option>
|
|
1221
|
+
</select>
|
|
1222
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="networkServer"></i>
|
|
1223
|
+
</div>
|
|
1224
|
+
|
|
1225
|
+
<div class="form-row">
|
|
1226
|
+
<label for="select-protocol" style="width: 120px">
|
|
1227
|
+
<i class="fa fa-plug"></i> Protocol
|
|
1228
|
+
</label>
|
|
1229
|
+
<select id="select-protocol">
|
|
1230
|
+
<option value="restAPIBacnet">REST API BACnet</option>
|
|
1231
|
+
<option value="bacnet">BACnet</option>
|
|
1232
|
+
<!--
|
|
1233
|
+
<option value="restAPIModbus">REST API Modbus</option>
|
|
1234
|
+
<option value="modbus">Modbus</option>
|
|
1235
|
+
<option value="restAPIModbus">REST API Modbus</option>
|
|
1236
|
+
<option value="modbus">Modbus</option>
|
|
1237
|
+
-->
|
|
1238
|
+
</select>
|
|
1239
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="protocol"></i>
|
|
1240
|
+
</div>
|
|
1241
|
+
|
|
1242
|
+
<div id="chirpstack-config">
|
|
1243
|
+
<div class="form-row">
|
|
1244
|
+
<h3><i class="fa fa-cog"></i> Chirpstack configuration</h3>
|
|
1245
|
+
</div>
|
|
1246
|
+
<div class="form-row">
|
|
1247
|
+
<label for="input-grpc-api-key" style="width: 120px"><i class="fa fa-key"></i> GRPC API Key</label>
|
|
1248
|
+
<input type="password" id="input-grpc-api-key" style="width: 260px"/>
|
|
1249
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="grpcApiKey"></i>
|
|
1250
|
+
</div>
|
|
1251
|
+
<div class="form-row">
|
|
1252
|
+
<label for="input-server-address" style="width: 120px"><i class="fa fa-server"></i> Server Address</label>
|
|
1253
|
+
<input type="text" id="input-server-address" style="width: 260px"/>
|
|
1254
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="serverAddress"></i>
|
|
1255
|
+
</div>
|
|
1256
|
+
<div class="form-row">
|
|
1257
|
+
<label for="input-grpc-port" style="width: 120px"><i class="fa fa-globe"></i> GRPC Port</label>
|
|
1258
|
+
<input type="number" id="input-grpc-port" style="width: 260px"/>
|
|
1259
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="grpcPort"></i>
|
|
1260
|
+
</div>
|
|
1261
|
+
</div>
|
|
1262
|
+
|
|
1263
|
+
<div id="bacnet-settings">
|
|
1264
|
+
<div class="form-row">
|
|
1265
|
+
<h3><i class="fa fa-cog"></i> Rest API BMS Settings</h3>
|
|
1266
|
+
</div>
|
|
1267
|
+
<div class="form-row">
|
|
1268
|
+
<label for="select-bacnet-api-model" style="width: 120px">
|
|
1269
|
+
<i class="fa fa-bolt"></i> Rest API Model
|
|
1270
|
+
</label>
|
|
1271
|
+
<select id="select-bacnet-api-model">
|
|
1272
|
+
<option value="distechControlsV2">Distech Control v2</option>
|
|
1273
|
+
</select>
|
|
1274
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="apiModel"></i>
|
|
1275
|
+
</div>
|
|
1276
|
+
<div class="form-row">
|
|
1277
|
+
<label for="input-bacnet-login" style="width: 120px"><i class="fa fa-user"></i> Login</label>
|
|
1278
|
+
<input type="text" id="input-bacnet-login" style="width: 270px"/>
|
|
1279
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="login"></i>
|
|
1280
|
+
</div>
|
|
1281
|
+
<div class="form-row">
|
|
1282
|
+
<label for="input-bacnet-password" style="width: 120px"><i class="fa fa-lock"></i> Password</label>
|
|
1283
|
+
<input type="password" id="input-bacnet-password" style="width: 270px"/>
|
|
1284
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="password"></i>
|
|
1285
|
+
</div>
|
|
1286
|
+
</div>
|
|
1287
|
+
</form>
|
|
1288
|
+
<div class="form-row">
|
|
1289
|
+
<h3><i class="fa fa-th-list"></i> Device list</h3>
|
|
1290
|
+
</div>
|
|
1291
|
+
|
|
1292
|
+
<div class="form-row">
|
|
1293
|
+
<button id="button-import-from-json" class="red-ui-button" style="height: 35px; margin-left: 0px;">
|
|
1294
|
+
<i class="fa fa-upload"></i> Import from JSON
|
|
1295
|
+
</button>
|
|
1296
|
+
</div>
|
|
1297
|
+
|
|
1298
|
+
<div id="device-list-container"></div>
|
|
1299
|
+
<div class="form-row">
|
|
1300
|
+
<button id="button-add-device" class="editor-button" style="margin-bottom: 10px"><i class="fa fa-plus"></i> Add device</button>
|
|
1301
|
+
</div>
|
|
1302
|
+
</script>
|
|
1303
|
+
|
|
1304
|
+
<!-- ============================================================
|
|
1305
|
+
======== IMPORT DEVICE LIST FROM JSON TEMPLATE TRAY ========
|
|
1306
|
+
============================================================ -->
|
|
1307
|
+
|
|
1308
|
+
<script type="text/html" id="import-from-json-template">
|
|
1309
|
+
<div class="form-horizontal" style="height: 660px; margin: 10px 20px;">
|
|
1310
|
+
<div class="form-row">
|
|
1311
|
+
<h3><i class="fa fa-upload"></i> Import device list from JSON</h3>
|
|
1312
|
+
</div>
|
|
1313
|
+
<div class="form-row">
|
|
1314
|
+
<input type="checkbox" id="checkbox-override-device-list" style="margin: 0px; width: auto;" />
|
|
1315
|
+
<label for="checkbox-override-device-list" style="width: 180px"> Overide previous device list</label>
|
|
1316
|
+
</div>
|
|
1317
|
+
<div style="height: 70vh; width: 600px;" class="node-text-editor" id="json-device-list-editor"></div>
|
|
1318
|
+
</div>
|
|
1319
|
+
</script>
|
|
1320
|
+
|
|
1321
|
+
<!-- ============================================================
|
|
1322
|
+
======== DEVICE CONFIGURATION TEMPLATE TRAY =========
|
|
1323
|
+
============================================================ -->
|
|
1324
|
+
|
|
1325
|
+
<script type="text/html" id="device-config-template">
|
|
1326
|
+
<div id="device-config" class="form-horizontal" autocomplete="off" style="height: 660px; margin: 10px 20px;">
|
|
1327
|
+
|
|
1328
|
+
<div class="form-row">
|
|
1329
|
+
<h3><i class="fa fa-cog"></i> <span id="device-config-title">Device Configuration</span></h3>
|
|
1330
|
+
</div>
|
|
1331
|
+
|
|
1332
|
+
<div class="form-row">
|
|
1333
|
+
<i class="fa fa-bug"></i> Debug <i class="fa fa-question-circle tooltip-icon" data-tooltip="debug"></i>
|
|
1334
|
+
</div>
|
|
1335
|
+
<div class="form-row checkbox-row">
|
|
1336
|
+
<input type="checkbox" id="debug-all" style="margin: 0px; width: auto;" />
|
|
1337
|
+
<label for="debug-all" style="width:auto"> All events</label>
|
|
1338
|
+
</div>
|
|
1339
|
+
<div class="form-row checkbox-row">
|
|
1340
|
+
<input type="checkbox" id="debug-up" style="margin: 0px; width: auto;" />
|
|
1341
|
+
<label for="debug-up" style="width:auto"> Uplink events</label>
|
|
1342
|
+
</div>
|
|
1343
|
+
<div class="form-row checkbox-row">
|
|
1344
|
+
<input type="checkbox" id="debug-down" style="margin: 0px; width: auto;" />
|
|
1345
|
+
<label for="debug-down" style="width:auto"> Downlink events</label>
|
|
1346
|
+
</div>
|
|
1347
|
+
<div class="form-row checkbox-row">
|
|
1348
|
+
<input type="checkbox" id="debug-creation" style="margin: 0px; width: auto;" />
|
|
1349
|
+
<label for="debug-creation" style="width:auto"> Creation events</label>
|
|
1350
|
+
</div>
|
|
1351
|
+
<div class="form-row checkbox-row">
|
|
1352
|
+
<input type="checkbox" id="debug-txTime" style="margin: 0px; width: auto;" />
|
|
1353
|
+
<label for="debug-txTime" style="width:auto"> Print TX time</label>
|
|
1354
|
+
</div>
|
|
1355
|
+
|
|
1356
|
+
<div class="form-row">
|
|
1357
|
+
<label for="select-class" style="width: 190px">
|
|
1358
|
+
<i class="fa fa-tag"></i> LoRaWAN device class
|
|
1359
|
+
</label>
|
|
1360
|
+
<select id="select-class">
|
|
1361
|
+
<option value="A">A</option>
|
|
1362
|
+
<option value="B">B</option>
|
|
1363
|
+
<option value="C">C</option>
|
|
1364
|
+
</select>
|
|
1365
|
+
</div>
|
|
1366
|
+
|
|
1367
|
+
<div class="form-row">
|
|
1368
|
+
<label for="input-max-dev-num" style="width: 190px"><i class="fa fa-arrow-up"></i> Maximum Device Number</label>
|
|
1369
|
+
<input type="number" id="input-max-dev-num" style="width: 80px;"/>
|
|
1370
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="maxDevNum"></i>
|
|
1371
|
+
</div>
|
|
1372
|
+
|
|
1373
|
+
<div class="form-row">
|
|
1374
|
+
<label for="select-flush-downlink-queue" style="width: 190px">
|
|
1375
|
+
<i class="fa fa-link"></i> Flush Downlink Queue
|
|
1376
|
+
</label>
|
|
1377
|
+
<select id="select-flush-downlink-queue">
|
|
1378
|
+
<option value="true">Yes</option>
|
|
1379
|
+
<option value="false">No</option>
|
|
1380
|
+
</select>
|
|
1381
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="flushDownlinkQueue"></i>
|
|
1382
|
+
</div>
|
|
1383
|
+
|
|
1384
|
+
|
|
1385
|
+
<div id="actility-configuration-device">
|
|
1386
|
+
<div class="form-row">
|
|
1387
|
+
<h3><i class="fa fa-cog"></i> Actility Configuration</h3>
|
|
1388
|
+
</div>
|
|
1389
|
+
<div class="form-row">
|
|
1390
|
+
<label for="input-pId-device" style="width: 60px"><i class="fa fa-tag"></i> pId</label>
|
|
1391
|
+
<input type="text" id="input-pId-device" style="width: 80px"/>
|
|
1392
|
+
</div>
|
|
1393
|
+
<div class="form-row">
|
|
1394
|
+
<label for="input-mId-device" style="width: 60px"><i class="fa fa-tag"></i> mId</label>
|
|
1395
|
+
<input type="text" id="input-mId-device" style="width: 80px"/>
|
|
1396
|
+
</div>
|
|
1397
|
+
<div class="form-row">
|
|
1398
|
+
<label for="input-ver-device" style="width: 60px"><i class="fa fa-hashtag"></i> Ver</label>
|
|
1399
|
+
<input type="text" id="input-ver-device" style="width: 80px"/>
|
|
1400
|
+
</div>
|
|
1401
|
+
</div>
|
|
1402
|
+
|
|
1403
|
+
<div class="form-row">
|
|
1404
|
+
<h3><i class="fa fa-cog"></i> <span id="instance-num-settings-title">Instance num settings</span></h3>
|
|
1405
|
+
</div>
|
|
1406
|
+
|
|
1407
|
+
<div class="form-row">
|
|
1408
|
+
<label for="input-offset-av" style="width: 140px"><i class="fa fa-arrows-h"></i> Offset AV</label>
|
|
1409
|
+
<input type="number" step="any" id="input-offset-av" style="width: 80px"/>
|
|
1410
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="offsetAV"></i>
|
|
1411
|
+
</div>
|
|
1412
|
+
<div class="form-row">
|
|
1413
|
+
<label for="input-offset-bv" style="width: 140px"><i class="fa fa-arrows-h"></i> Offset BV</label>
|
|
1414
|
+
<input type="number" step="any" id="input-offset-bv" style="width: 80px"/>
|
|
1415
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="offsetBV"></i>
|
|
1416
|
+
</div>
|
|
1417
|
+
<div class="form-row">
|
|
1418
|
+
<label for="input-instance-range-av" style="width: 140px"><i class="fa fa-arrows"></i> Instance Range AV</label>
|
|
1419
|
+
<input type="number" step="any" id="input-instance-range-av" style="width: 80px"/>
|
|
1420
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="instanceRangeAV"></i>
|
|
1421
|
+
</div>
|
|
1422
|
+
<div class="form-row">
|
|
1423
|
+
<label for="input-instance-range-bv" style="width: 140px"><i class="fa fa-arrows"></i> Instance Range BV</label>
|
|
1424
|
+
<input type="number" step="any" id="input-instance-range-bv" style="width: 80px"/>
|
|
1425
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="instanceRangeBV"></i>
|
|
1426
|
+
</div>
|
|
1427
|
+
|
|
1428
|
+
<div class="form-row">
|
|
1429
|
+
<h3><i class="fa fa-th-list"></i> BACnet Object list</h3>
|
|
1430
|
+
</div>
|
|
1431
|
+
|
|
1432
|
+
<div id="object-list-container"></div>
|
|
1433
|
+
|
|
1434
|
+
<div class="form-row">
|
|
1435
|
+
<button type="button" id="add-object-btn" class="red-ui-button" style="margin-top: 10px; margin-bottom: 10px">
|
|
1436
|
+
<i class="fa fa-plus"></i> Add Object
|
|
1437
|
+
</button>
|
|
1438
|
+
</div>
|
|
1439
|
+
</div>
|
|
1440
|
+
</script>
|
|
1441
|
+
|
|
1442
|
+
<!-- ============================================================
|
|
1443
|
+
======== OBJECT CONFIGURATION TEMPLATE TRAY =========
|
|
1444
|
+
============================================================ -->
|
|
1445
|
+
|
|
1446
|
+
<script type="text/html" id="object-config-template">
|
|
1447
|
+
<div id="device-config" class="form-horizontal" autocomplete="off" style="height: 660px; margin: 10px 20px;">
|
|
1448
|
+
<div class="form-row">
|
|
1449
|
+
<h3><i class="fa fa-cog"></i> <span id="object-config-title">Object Configuration</span></h3>
|
|
1450
|
+
</div>
|
|
1451
|
+
|
|
1452
|
+
<div class="form-row">
|
|
1453
|
+
<label for="input-lorawan-payload-name" style="width: 180px"><i class="fa fa-tag"></i> LoRaWAN Payload name</label>
|
|
1454
|
+
<input type="text" id="input-lorawan-payload-name" style="width: 265px"/>
|
|
1455
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="lorawanPayloadName"></i>
|
|
1456
|
+
</div>
|
|
1457
|
+
|
|
1458
|
+
<div class="form-row">
|
|
1459
|
+
<label for="select-object-type" style="width: 160px">
|
|
1460
|
+
<i class="fa fa-asterisk"></i> Object type
|
|
1461
|
+
</label>
|
|
1462
|
+
<select id="select-object-type" style="width: 130px">
|
|
1463
|
+
<option value="analogValue">Analog value</option>
|
|
1464
|
+
<option value="binaryValue">Binary value</option>
|
|
1465
|
+
</select>
|
|
1466
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="objectType"></i>
|
|
1467
|
+
</div>
|
|
1468
|
+
|
|
1469
|
+
<div class="form-row">
|
|
1470
|
+
<label for="select-assignement-mode" style="width: 160px">
|
|
1471
|
+
<i class="fa fa-toggle-up"></i> Assignation mode
|
|
1472
|
+
</label>
|
|
1473
|
+
<select id="select-assignement-mode" style="width: 130px">
|
|
1474
|
+
<option value="auto">Auto</option>
|
|
1475
|
+
<option value="manual">Manual</option>
|
|
1476
|
+
</select>
|
|
1477
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="assignementMode"></i>
|
|
1478
|
+
</div>
|
|
1479
|
+
|
|
1480
|
+
<div class="form-row">
|
|
1481
|
+
<label for="input-instance-num" style="width: 160px"><i class="fa fa-hashtag"></i> <span id="span-instance-num">Instance num</span></label>
|
|
1482
|
+
<input type="number" id="input-instance-num" style="width: 130px"/>
|
|
1483
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="instanceNum"></i>
|
|
1484
|
+
</div>
|
|
1485
|
+
|
|
1486
|
+
<div class="form-row">
|
|
1487
|
+
<label for="select-data-direction" style="width: 160px">
|
|
1488
|
+
<i class="fa fa-rss"></i> Data direction
|
|
1489
|
+
</label>
|
|
1490
|
+
<select id="select-data-direction" style="width: 130px">
|
|
1491
|
+
<option value="uplink">Uplink</option>
|
|
1492
|
+
<option value="downlink">Downlink</option>
|
|
1493
|
+
</select>
|
|
1494
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="dataDirection"></i>
|
|
1495
|
+
</div>
|
|
1496
|
+
|
|
1497
|
+
<div id="downlink-configuration-container">
|
|
1498
|
+
|
|
1499
|
+
<div class="form-row">
|
|
1500
|
+
<h3><i class="fa fa-cog"></i> Downlink configuration</span></h3>
|
|
1501
|
+
</div>
|
|
1502
|
+
|
|
1503
|
+
<div class="form-row">
|
|
1504
|
+
<label for="input-downlink-port" style="width: 140px"><i class="fa fa-download"></i> Downlink FPort</label>
|
|
1505
|
+
<input type="number" id="input-downlink-port" style="width: 80px"/>
|
|
1506
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="downlinkPort"></i>
|
|
1507
|
+
</div>
|
|
1508
|
+
|
|
1509
|
+
<div class="form-row">
|
|
1510
|
+
<label for="select-downlink-priority" style="width: 140px"><i class="fa fa-star"></i> Priority</label>
|
|
1511
|
+
<select id="select-downlink-priority" style="width: 80px">
|
|
1512
|
+
<option value="low">Low</option>
|
|
1513
|
+
<option value="high">High</option>
|
|
1514
|
+
</select>
|
|
1515
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="priority"></i>
|
|
1516
|
+
</div>
|
|
1517
|
+
|
|
1518
|
+
<div class="form-row">
|
|
1519
|
+
<label for="select-downlink-strategies" style="width: 140px">
|
|
1520
|
+
<i class="fa fa-arrow-circle-o-right"></i> Downlink strategies
|
|
1521
|
+
</label>
|
|
1522
|
+
<select id="select-downlink-strategies" style="width: 380px">
|
|
1523
|
+
<option value="compareToUplinkObject">On compare to the following uplink Object</option>
|
|
1524
|
+
<option value="compareToUplinkObjectWithinRange">On compare to the following uplink Object within range</option>
|
|
1525
|
+
<option value="onChangeOfThisValue">On change of this value on the BMS</option>
|
|
1526
|
+
<option value="onChangeOfThisValueWithinRange">On change of this value within range on the BMS</option>
|
|
1527
|
+
</select>
|
|
1528
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="downlinkStrategies"></i>
|
|
1529
|
+
</div>
|
|
1530
|
+
|
|
1531
|
+
<div class="form-row" id="uplink-object-container">
|
|
1532
|
+
<label for="input-compare-uplink-object" style="width: 140px"><i class="fa fa-tag"></i> Uplink object name</label>
|
|
1533
|
+
<input type="text" id="input-compare-uplink-value" style="width: 140px"/>
|
|
1534
|
+
</div>
|
|
1535
|
+
|
|
1536
|
+
<div class="form-row" id="value-range-container">
|
|
1537
|
+
<label for="input-change-value-range" style="width: 140px"><i class="fa fa-arrows-h"></i> Value Range</label>
|
|
1538
|
+
<input type="number" id="input-change-value-range-min" style="width: 80px"/>
|
|
1539
|
+
<i class="fa fa-arrows-h" style="margin: 0 5px"></i>
|
|
1540
|
+
<input type="number" id="input-change-value-range-max" style="width: 80px"/>
|
|
1541
|
+
</div>
|
|
1542
|
+
|
|
1543
|
+
<div class="form-row" id="analog-value-container">
|
|
1544
|
+
<label for="input-analog-value" style="width: 140px"><i class="fa fa-hashtag"></i> Default Value</label>
|
|
1545
|
+
<input type="number" id="input-analog-value" style="width: 130px"/>
|
|
1546
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="defaultValue"></i>
|
|
1547
|
+
</div>
|
|
1548
|
+
|
|
1549
|
+
<div class="form-row" id="binary-value-container">
|
|
1550
|
+
<label for="input-binary-value" style="width: 140px">
|
|
1551
|
+
<i class="fa fa-hashtag"></i> Default Value
|
|
1552
|
+
</label>
|
|
1553
|
+
<select id="input-binary-value" style="width: 130px">
|
|
1554
|
+
<option value="0">0</option>
|
|
1555
|
+
<option value="1">1</option>
|
|
1556
|
+
</select>
|
|
1557
|
+
<i class="fa fa-question-circle tooltip-icon" data-tooltip="defaultValue"></i>
|
|
1558
|
+
</div>
|
|
1559
|
+
|
|
1560
|
+
<div id="actility-configuration-object">
|
|
1561
|
+
<div class="form-row">
|
|
1562
|
+
<h4>Actility Configuration</h4>
|
|
1563
|
+
</div>
|
|
1564
|
+
<div class="form-row">
|
|
1565
|
+
<label for="input-pId-device" style="width: 60px"><i class="fa fa-tag"></i> pId</label>
|
|
1566
|
+
<input type="text" id="input-pId-device" style="width: 80px"/>
|
|
1567
|
+
</div>
|
|
1568
|
+
<div class="form-row">
|
|
1569
|
+
<label for="input-mId-device" style="width: 60px"><i class="fa fa-tag"></i> mId</label>
|
|
1570
|
+
<input type="text" id="input-mId-device" style="width: 80px"/>
|
|
1571
|
+
</div>
|
|
1572
|
+
<div class="form-row">
|
|
1573
|
+
<label for="input-ver-device" style="width: 60px"><i class="fa fa-hashtag"></i> Ver</label>
|
|
1574
|
+
<input type="text" id="input-ver-device" style="width: 80px"/>
|
|
1575
|
+
</div>
|
|
1576
|
+
</div>
|
|
1577
|
+
|
|
1578
|
+
</div>
|
|
1579
|
+
</div>
|
|
1580
|
+
</script>
|
|
1581
|
+
|
|
1582
|
+
<!-- ============================================================
|
|
1583
|
+
================ NODE DESCRIPTION ===================
|
|
1584
|
+
============================================================ -->
|
|
1585
|
+
|
|
1586
|
+
<script type="text/x-red" data-help-name="LoRaBAC">
|
|
1587
|
+
<p>LoRaBAC Node</p>
|
|
1588
|
+
</script>
|