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.

@@ -0,0 +1,293 @@
1
+ /**
2
+ * BACnet Point Node
3
+ *
4
+ * This node represents a BACnet object (Analog Value, Binary Value, etc.)
5
+ * that is exposed through the BACnet Server. It:
6
+ * - Registers itself with the parent bacnet-server config node
7
+ * - Accepts input messages to update the present value
8
+ * - Emits output messages when external clients read/write the point
9
+ * - Supports Priority Array for commandable objects
10
+ * - Supports COV (Change of Value) threshold configuration
11
+ */
12
+
13
+ module.exports = function (RED) {
14
+
15
+ // BACnet object type constants
16
+ const ObjectTypes = {
17
+ 'analogInput': 0,
18
+ 'analogOutput': 1,
19
+ 'analogValue': 2,
20
+ 'binaryInput': 3,
21
+ 'binaryOutput': 4,
22
+ 'binaryValue': 5,
23
+ 'multiStateValue': 19
24
+ };
25
+
26
+ // Object types that support Priority Array (commandable objects)
27
+ const CommandableTypes = ['analogOutput', 'analogValue', 'binaryOutput', 'binaryValue'];
28
+
29
+ function BacnetPointNode(config) {
30
+ RED.nodes.createNode(this, config);
31
+ const node = this;
32
+
33
+ // Get the server config node
34
+ node.server = RED.nodes.getNode(config.server);
35
+
36
+ if (!node.server) {
37
+ node.status({ fill: 'red', shape: 'ring', text: 'No server configured' });
38
+ node.error('No BACnet server configured');
39
+ return;
40
+ }
41
+
42
+ // Configuration
43
+ node.objectType = config.objectType || 'analogValue';
44
+ node.objectTypeId = ObjectTypes[node.objectType];
45
+ node.instanceNumber = parseInt(config.instanceNumber) || 0;
46
+ node.objectName = config.objectName || `${node.objectType}_${node.instanceNumber}`;
47
+ node.initialValue = parseFloat(config.initialValue) || 0;
48
+ node.units = config.units !== '' ? parseInt(config.units) : undefined;
49
+ node.description = config.description || '';
50
+ node.writable = config.writable !== false;
51
+ node.usePriorityArray = config.usePriorityArray === true && CommandableTypes.includes(node.objectType);
52
+ node.relinquishDefault = parseFloat(config.relinquishDefault) || 0;
53
+ node.covIncrement = parseFloat(config.covIncrement) || 0;
54
+ node.numberOfStates = parseInt(config.numberOfStates) || 3;
55
+ node.stateText = config.stateText ? config.stateText.split(',').map(s => s.trim()) : undefined;
56
+
57
+ // Output options
58
+ node.outputOnRead = config.outputOnRead === true;
59
+ node.outputOnWrite = config.outputOnWrite !== false;
60
+ node.outputOnCov = config.outputOnCov === true;
61
+
62
+ // Current value
63
+ node.currentValue = node.initialValue;
64
+
65
+ // Register with server
66
+ node.registerWithServer = function () {
67
+ if (!node.server) return;
68
+
69
+ const objectData = {
70
+ objectType: node.objectTypeId,
71
+ instanceNumber: node.instanceNumber,
72
+ objectName: node.objectName,
73
+ initialValue: node.initialValue,
74
+ units: node.units,
75
+ description: node.description,
76
+ writable: node.writable,
77
+ priorityArray: node.usePriorityArray,
78
+ relinquishDefault: node.relinquishDefault,
79
+ covIncrement: node.covIncrement,
80
+ numberOfStates: node.objectType === 'multiStateValue' ? node.numberOfStates : undefined,
81
+ stateText: node.objectType === 'multiStateValue' ? node.stateText : undefined
82
+ };
83
+
84
+ node.server.registerObject(objectData, node);
85
+ node.updateStatus();
86
+ };
87
+
88
+ // Update node status
89
+ node.updateStatus = function () {
90
+ const typeLabel = node.objectType.replace(/([A-Z])/g, ' $1').trim();
91
+ node.status({
92
+ fill: 'green',
93
+ shape: 'dot',
94
+ text: `${typeLabel} ${node.instanceNumber}: ${formatValue(node.currentValue)}`
95
+ });
96
+ };
97
+
98
+ // Format value for status display
99
+ function formatValue(val) {
100
+ if (typeof val === 'number') {
101
+ if (Number.isInteger(val)) {
102
+ return val.toString();
103
+ }
104
+ return val.toFixed(2);
105
+ }
106
+ return String(val);
107
+ }
108
+
109
+ // Emit message when external client reads the point
110
+ node.emitReadEvent = function (value) {
111
+ if (!node.outputOnRead) return;
112
+
113
+ const msg = {
114
+ payload: value,
115
+ topic: node.objectName,
116
+ bacnet: {
117
+ event: 'read',
118
+ objectType: node.objectType,
119
+ objectTypeId: node.objectTypeId,
120
+ instanceNumber: node.instanceNumber,
121
+ objectName: node.objectName
122
+ }
123
+ };
124
+
125
+ node.send(msg);
126
+ };
127
+
128
+ // Emit message when external client writes to the point
129
+ node.emitWriteEvent = function (value, priority, address) {
130
+ node.currentValue = value;
131
+ node.updateStatus();
132
+
133
+ if (!node.outputOnWrite) return;
134
+
135
+ const msg = {
136
+ payload: value,
137
+ topic: node.objectName,
138
+ bacnet: {
139
+ event: 'write',
140
+ objectType: node.objectType,
141
+ objectTypeId: node.objectTypeId,
142
+ instanceNumber: node.instanceNumber,
143
+ objectName: node.objectName,
144
+ priority: priority,
145
+ source: address
146
+ }
147
+ };
148
+
149
+ node.send(msg);
150
+ };
151
+
152
+ // Emit message on COV notification
153
+ node.emitCovEvent = function (value) {
154
+ if (!node.outputOnCov) return;
155
+
156
+ const msg = {
157
+ payload: value,
158
+ topic: node.objectName,
159
+ bacnet: {
160
+ event: 'cov',
161
+ objectType: node.objectType,
162
+ objectTypeId: node.objectTypeId,
163
+ instanceNumber: node.instanceNumber,
164
+ objectName: node.objectName
165
+ }
166
+ };
167
+
168
+ node.send(msg);
169
+ };
170
+
171
+ // Handle incoming messages to update value
172
+ node.on('input', function (msg, send, done) {
173
+ let newValue;
174
+ let priority = 16;
175
+
176
+ // Extract value from message
177
+ if (typeof msg.payload === 'object' && msg.payload !== null) {
178
+ // Structured payload
179
+ if (msg.payload.value !== undefined) {
180
+ newValue = msg.payload.value;
181
+ }
182
+ if (msg.payload.priority !== undefined) {
183
+ priority = parseInt(msg.payload.priority);
184
+ if (priority < 1 || priority > 16) priority = 16;
185
+ }
186
+ } else {
187
+ // Simple payload
188
+ newValue = msg.payload;
189
+ }
190
+
191
+ // Check for priority in msg.bacnet
192
+ if (msg.bacnet && msg.bacnet.priority !== undefined) {
193
+ priority = parseInt(msg.bacnet.priority);
194
+ if (priority < 1 || priority > 16) priority = 16;
195
+ }
196
+
197
+ // Validate and convert value based on object type
198
+ if (newValue === undefined || newValue === null) {
199
+ // Null value means relinquish at this priority (for priority array)
200
+ if (node.usePriorityArray) {
201
+ newValue = null;
202
+ } else {
203
+ if (done) done();
204
+ return;
205
+ }
206
+ } else {
207
+ switch (node.objectType) {
208
+ case 'binaryInput':
209
+ case 'binaryOutput':
210
+ case 'binaryValue':
211
+ // Convert to binary (0 or 1)
212
+ if (typeof newValue === 'boolean') {
213
+ newValue = newValue ? 1 : 0;
214
+ } else if (typeof newValue === 'string') {
215
+ const lower = newValue.toLowerCase();
216
+ if (lower === 'true' || lower === 'on' || lower === 'active' || lower === '1') {
217
+ newValue = 1;
218
+ } else {
219
+ newValue = 0;
220
+ }
221
+ } else {
222
+ newValue = newValue ? 1 : 0;
223
+ }
224
+ break;
225
+
226
+ case 'multiStateValue':
227
+ // Convert to integer, ensure within valid range
228
+ newValue = parseInt(newValue);
229
+ if (isNaN(newValue) || newValue < 1) newValue = 1;
230
+ if (newValue > node.numberOfStates) newValue = node.numberOfStates;
231
+ break;
232
+
233
+ case 'analogInput':
234
+ case 'analogOutput':
235
+ case 'analogValue':
236
+ default:
237
+ // Convert to float
238
+ newValue = parseFloat(newValue);
239
+ if (isNaN(newValue)) newValue = 0;
240
+ break;
241
+ }
242
+ }
243
+
244
+ // Update the server object
245
+ if (node.server) {
246
+ // For priority array objects, we need to handle priority
247
+ if (node.usePriorityArray) {
248
+ // Update through the server which handles priority array
249
+ const objectId = { type: node.objectTypeId, instance: node.instanceNumber };
250
+ const result = node.server.setPropertyValue(
251
+ objectId,
252
+ 85, // PRESENT_VALUE property ID
253
+ [{ type: newValue === null ? 0 : 4, value: newValue }],
254
+ priority
255
+ );
256
+ if (result.success) {
257
+ node.currentValue = result.value;
258
+ }
259
+ } else {
260
+ node.server.updateObjectValue(node.objectTypeId, node.instanceNumber, newValue);
261
+ node.currentValue = newValue;
262
+ }
263
+ } else {
264
+ node.currentValue = newValue;
265
+ }
266
+
267
+ node.updateStatus();
268
+
269
+ if (done) done();
270
+ });
271
+
272
+ // Wait for server to be ready before registering
273
+ if (node.server.client) {
274
+ // Server already initialized
275
+ node.registerWithServer();
276
+ } else {
277
+ // Wait for server ready event
278
+ node.server.on('ready', function () {
279
+ node.registerWithServer();
280
+ });
281
+ }
282
+
283
+ // Handle node close
284
+ node.on('close', function (done) {
285
+ if (node.server) {
286
+ node.server.unregisterObject(node.objectTypeId, node.instanceNumber);
287
+ }
288
+ done();
289
+ });
290
+ }
291
+
292
+ RED.nodes.registerType('bacnet-point', BacnetPointNode);
293
+ };
@@ -0,0 +1,138 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('bacnet-server', {
3
+ category: 'config',
4
+ defaults: {
5
+ name: { value: '' },
6
+ deviceId: { value: 123456, required: true, validate: RED.validators.number() },
7
+ deviceName: { value: 'Node-RED-BACnet', required: true },
8
+ vendorId: { value: 999, required: true, validate: RED.validators.number() },
9
+ port: { value: 47808, required: true, validate: RED.validators.number() },
10
+ interface: { value: '' },
11
+ broadcastAddress: { value: '255.255.255.255' }
12
+ },
13
+ label: function () {
14
+ return this.name || this.deviceName || 'BACnet Server';
15
+ },
16
+ oneditprepare: function () {
17
+ var node = this;
18
+
19
+ // Validate device ID range (0 to 4194302)
20
+ $('#node-config-input-deviceId').on('change', function () {
21
+ var val = parseInt($(this).val());
22
+ if (isNaN(val) || val < 0 || val > 4194302) {
23
+ RED.notify('Device ID must be between 0 and 4194302', 'error');
24
+ $(this).val(123456);
25
+ }
26
+ });
27
+
28
+ // Validate port range
29
+ $('#node-config-input-port').on('change', function () {
30
+ var val = parseInt($(this).val());
31
+ if (isNaN(val) || val < 1 || val > 65535) {
32
+ RED.notify('Port must be between 1 and 65535', 'error');
33
+ $(this).val(47808);
34
+ }
35
+ });
36
+
37
+ // Validate vendor ID range (0 to 65535)
38
+ $('#node-config-input-vendorId').on('change', function () {
39
+ var val = parseInt($(this).val());
40
+ if (isNaN(val) || val < 0 || val > 65535) {
41
+ RED.notify('Vendor ID must be between 0 and 65535', 'error');
42
+ $(this).val(999);
43
+ }
44
+ });
45
+ }
46
+ });
47
+ </script>
48
+
49
+ <script type="text/html" data-template-name="bacnet-server">
50
+ <div class="form-row">
51
+ <label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
52
+ <input type="text" id="node-config-input-name" placeholder="Optional name">
53
+ </div>
54
+
55
+ <div class="form-row">
56
+ <h4><i class="fa fa-server"></i> Device Configuration</h4>
57
+ </div>
58
+
59
+ <div class="form-row">
60
+ <label for="node-config-input-deviceId"><i class="fa fa-hashtag"></i> Device ID</label>
61
+ <input type="number" id="node-config-input-deviceId" min="0" max="4194302" style="width: 120px;">
62
+ <span class="form-tips" style="margin-left: 10px;">Instance number (0-4194302)</span>
63
+ </div>
64
+
65
+ <div class="form-row">
66
+ <label for="node-config-input-deviceName"><i class="fa fa-bookmark"></i> Device Name</label>
67
+ <input type="text" id="node-config-input-deviceName" placeholder="Node-RED-BACnet">
68
+ </div>
69
+
70
+ <div class="form-row">
71
+ <label for="node-config-input-vendorId"><i class="fa fa-building"></i> Vendor ID</label>
72
+ <input type="number" id="node-config-input-vendorId" min="0" max="65535" style="width: 100px;">
73
+ </div>
74
+
75
+ <div class="form-row">
76
+ <h4><i class="fa fa-plug"></i> Network Configuration</h4>
77
+ </div>
78
+
79
+ <div class="form-row">
80
+ <label for="node-config-input-port"><i class="fa fa-globe"></i> UDP Port</label>
81
+ <input type="number" id="node-config-input-port" min="1" max="65535" style="width: 100px;">
82
+ <span class="form-tips" style="margin-left: 10px;">Default: 47808 (0xBAC0)</span>
83
+ </div>
84
+
85
+ <div class="form-row">
86
+ <label for="node-config-input-interface"><i class="fa fa-ethernet"></i> Interface</label>
87
+ <input type="text" id="node-config-input-interface" placeholder="Leave empty for all interfaces">
88
+ <span class="form-tips" style="margin-left: 10px;">e.g., 192.168.1.100</span>
89
+ </div>
90
+
91
+ <div class="form-row">
92
+ <label for="node-config-input-broadcastAddress"><i class="fa fa-broadcast-tower"></i> Broadcast</label>
93
+ <input type="text" id="node-config-input-broadcastAddress" placeholder="255.255.255.255">
94
+ </div>
95
+ </script>
96
+
97
+ <script type="text/html" data-help-name="bacnet-server">
98
+ <p>BACnet Server configuration node that exposes Node-RED as a BACnet device on the network.</p>
99
+
100
+ <h3>Configuration</h3>
101
+ <dl class="message-properties">
102
+ <dt>Device ID <span class="property-type">number</span></dt>
103
+ <dd>The BACnet device instance number (0-4194302). Must be unique on the network.</dd>
104
+
105
+ <dt>Device Name <span class="property-type">string</span></dt>
106
+ <dd>The name of this BACnet device as it will appear to other devices.</dd>
107
+
108
+ <dt>Vendor ID <span class="property-type">number</span></dt>
109
+ <dd>The BACnet vendor identifier (0-65535). Use 999 for experimental/private use.</dd>
110
+
111
+ <dt>UDP Port <span class="property-type">number</span></dt>
112
+ <dd>The UDP port for BACnet communication. Default is 47808 (0xBAC0).</dd>
113
+
114
+ <dt>Interface <span class="property-type">string</span></dt>
115
+ <dd>Network interface IP to bind to. Leave empty to listen on all interfaces.</dd>
116
+
117
+ <dt>Broadcast Address <span class="property-type">string</span></dt>
118
+ <dd>The broadcast address for BACnet discovery. Usually 255.255.255.255.</dd>
119
+ </dl>
120
+
121
+ <h3>Details</h3>
122
+ <p>This configuration node manages the BACnet server infrastructure:</p>
123
+ <ul>
124
+ <li><b>Who-Is / I-Am</b>: Responds to device discovery requests</li>
125
+ <li><b>Read Property</b>: Allows external clients to read object values</li>
126
+ <li><b>Write Property</b>: Allows external clients to write object values</li>
127
+ <li><b>COV Subscriptions</b>: Supports Change of Value notifications</li>
128
+ </ul>
129
+
130
+ <p>Use <code>bacnet-point</code> nodes to create BACnet objects (Analog Value, Binary Value, etc.)
131
+ that are exposed through this server.</p>
132
+
133
+ <h3>References</h3>
134
+ <ul>
135
+ <li><a href="http://www.bacnet.org/">BACnet International</a></li>
136
+ <li><a href="https://github.com/BACnet-IT/node-bacnet">node-bacnet library</a></li>
137
+ </ul>
138
+ </script>