@halsystems/red-bacnet 1.0.23 → 1.1.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.1.0]
4
+ ### Changed
5
+ - Implemented auto resize batch size when querying using readPropertyMultiple to improve performance
6
+ - Added readPropertyMultiple to read object list to reduce query speed
7
+ - Changed strategy when discovering device name, implemented delay instead of burst to reduce traffic congestion and yield better result
8
+ - Force use readProperty to read multistate object to improve result
9
+
10
+ ### Fixed
11
+ - Failure to discover device name for Forwarded NPDU devices
12
+ - Missed bacnet objects when using discover point
13
+
3
14
  ## [1.0.22]
4
15
  ### Fixed
5
16
  - Removed console.log in Read Point
package/common/bacnet.js CHANGED
@@ -18,7 +18,8 @@ const { getErrMsg } = require('@root/common/func.js')
18
18
  // ---------------------------------- export ----------------------------------
19
19
  module.exports = {
20
20
  /**
21
- * Reads the object list of a device.
21
+ * Reads the object list of a device using optimized batch reading.
22
+ * First tries readPropertyMultiple for faster performance, falls back to sequential reading if failed.
22
23
  * @param {BacnetClient} client
23
24
  * @param {object} device
24
25
  * eg:{
@@ -38,15 +39,98 @@ module.exports = {
38
39
  readObjectList: async function (client, device) {
39
40
  const objectId = { type: baEnum.ObjectType.DEVICE, instance: device.deviceId };
40
41
  const propertyId = baEnum.PropertyIdentifier.OBJECT_LIST;
41
- return await module.exports.readPropertyReturnArr(client, device, objectId, propertyId);
42
+
43
+ let result = await module.exports.readPropertyMultipleReturnArr(client, device, objectId, propertyId);
44
+ if (result.length == 0) {
45
+ result = await module.exports.readPropertyReturnArr(client, device, objectId, propertyId);
46
+ }
47
+
48
+ return result;
49
+ },
50
+
51
+ /**
52
+ * Reads a property that returns an array using readPropertyMultiple for better performance.
53
+ * Attempts to read the property in batches to minimize round trips.
54
+ * @param {BacnetClient} client
55
+ * @param {object} device
56
+ * @param {object} objectId
57
+ * @param {number} propertyId
58
+ * @returns array of objects
59
+ * @async
60
+ */
61
+ readPropertyMultipleReturnArr: async function (client, device, objectId, propertyId) {
62
+ let addressSet = device.ipAddress;
63
+ if (device.macAddress != null && device.network != null) {
64
+ addressSet = {
65
+ ip: device.ipAddress,
66
+ adr: device.macAddress,
67
+ net: device.network
68
+ };
69
+ }
70
+
71
+ const result = [];
72
+ let healthy = true;
73
+
74
+ // helper for single batch request
75
+ async function readBatch(arrayIndex) {
76
+ const reqArr = [{
77
+ objectId: objectId,
78
+ properties: [{
79
+ id: propertyId,
80
+ index: arrayIndex != null ? arrayIndex : baEnum.ASN1_ARRAY_ALL
81
+ }]
82
+ }];
83
+
84
+ return new Promise((resolve, reject) => {
85
+ client.readPropertyMultiple(
86
+ addressSet,
87
+ reqArr,
88
+ { maxApdu: device.maxApdu },
89
+ (err, value) => {
90
+ if (err) return reject(err);
91
+ resolve(value);
92
+ }
93
+ );
94
+ });
95
+ }
96
+
97
+ // try reading all at once first
98
+ try {
99
+ const value = await readBatch(baEnum.ASN1_ARRAY_ALL);
100
+ if (value?.values?.[0]?.values?.[0]?.value) {
101
+ return value.values[0].values[0].value;
102
+ }
103
+ } catch (err) {
104
+ void err
105
+ healthy = false;
106
+ }
107
+
108
+ // try using index read
109
+ if (!healthy) {
110
+ for (let i = 0; ; i++) {
111
+ try {
112
+ const res = await readBatch(i);
113
+ const item = res?.values?.[0]?.values?.[0]?.value?.[0];
114
+
115
+ if (item?.type === 105) break
116
+
117
+ result.push(item);
118
+ console.log(item)
119
+ } catch (err) {
120
+ void err
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ return result;
42
127
  },
43
128
 
44
129
  /**
45
130
  * Smart read multiple properties from a BACnet device with concurrent read feature and various read methods
46
131
  * readMethod
47
132
  * 0: use readProperty only
48
- * 1: try readPropertyMultiple with consevative query size, fallback to readProperty if failed
49
- * 2: try readPropertyMultiple twice with high and conservative query size, fallback to readProperty if failed
133
+ * 1: try readPropertyMultiple, reduce query size if failed, and fallback to readProperty if query size reduced to 1
50
134
  * @param {BacnetClient} client
51
135
  * @param {object} device
52
136
  * eg:{
@@ -95,16 +179,12 @@ module.exports = {
95
179
  if (readMethod > 0) {
96
180
  // calculate batch sizes
97
181
  if (device.maxApdu == null) // default batch size if null or undefined
98
- batchSizes.add(20).add(5);
182
+ batchSizes.add(20);
99
183
  else {
100
- const perValueBytes = [30, 50] // typical numeric point is 17 byte
184
+ const perValueBytes = [30] // typical numeric point is 17 byte
101
185
  for (let x = 0; x < perValueBytes.length; x++)
102
186
  batchSizes.add(Math.trunc(device.maxApdu / perValueBytes[x]))
103
187
  }
104
-
105
- // if readMethod === 1
106
- if (readMethod === 1 && batchSizes.size > 1)
107
- batchSizes = new Set([...batchSizes].slice(-1));
108
188
  }
109
189
 
110
190
  for (const batchSize of batchSizes) {
@@ -112,6 +192,7 @@ module.exports = {
112
192
  let first = true
113
193
  let reqArrBatch
114
194
  let batchCount
195
+ let currBatchSize = batchSize
115
196
 
116
197
  do { // use current batch size until query failed
117
198
  // get ready for next batch
@@ -132,7 +213,7 @@ module.exports = {
132
213
  if (reqArrIndexNext >= reqArr.length)
133
214
  break
134
215
 
135
- if (batchCount + reqArr[reqArrIndexNext].properties.length > batchSize)
216
+ if (batchCount + reqArr[reqArrIndexNext].properties.length > currBatchSize)
136
217
  break
137
218
 
138
219
  reqArrBatch.push(reqArr[reqArrIndexNext])
@@ -143,6 +224,14 @@ module.exports = {
143
224
  // read batch block
144
225
  const value = await module.exports.readPropertyMultple(client, device, reqArrBatch)
145
226
  .catch(() => {
227
+ // Reduce batch size by half (minimum 1)
228
+ currBatchSize = Math.max(1, Math.floor(currBatchSize / 2))
229
+
230
+ // If batch size is too small, mark as unhealthy
231
+ if (currBatchSize <= 1) {
232
+ healthy = false
233
+ }
234
+
146
235
  reqArrIndexNext = reqArrIndex
147
236
  healthy = false
148
237
  })
@@ -196,7 +285,7 @@ module.exports = {
196
285
  return value;
197
286
  } catch (err) {
198
287
  failedCount++;
199
- if (readMethod < 2 && failedCount >= singleReadFailedRetry)
288
+ if (readMethod < 1 && failedCount >= singleReadFailedRetry)
200
289
  throw err
201
290
  }
202
291
  }
@@ -317,7 +406,7 @@ module.exports = {
317
406
  * eg: 85
318
407
  * @returns object
319
408
  * eg: { len: 11, objectId: { type: 19, instance: 1 },
320
- * property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 } ]}
409
+ * property: { id: 85, index: 4294967295 }, values: [ { type: 2, value: 3 }]}
321
410
  * @async
322
411
  */
323
412
  readProperty: async function (client, device, objectId, propertyId) {
@@ -485,4 +574,4 @@ module.exports = {
485
574
  });
486
575
  });
487
576
  },
488
- }
577
+ }
@@ -2,6 +2,7 @@
2
2
  require('./_alias.js');
3
3
 
4
4
  const { EVENT_OUTPUT, EVENT_ERROR } = require('@root/common/core/constant.js');
5
+ const { delay } = require('@root/common/core/util.js');
5
6
 
6
7
  // ---------------------------------- type def ----------------------------------
7
8
  /**
@@ -43,8 +44,7 @@ module.exports = {
43
44
  await Promise.race(executing);
44
45
  }
45
46
 
46
- // need delay?
47
- // if (typeof delay === 'function') await delay(1);
47
+ await delay(50)
48
48
  }
49
49
 
50
50
  await Promise.allSettled(executing);
@@ -31,10 +31,9 @@ module.exports = {
31
31
  * @param {number} lowLimit - Low limit of the network range.
32
32
  * @param {number} highLimit - High limit of the network range.
33
33
  * @param {number} whoIsTimeout - Who is request timeout in milliseconds.
34
- * @param {number} readDeviceNameTimeout - Read device name timeout in milliseconds.
35
34
  */
36
35
  constructor(
37
- client, eventEmitter, network, lowLimit, highLimit, whoIsTimeout, readDeviceNameTimeout,
36
+ client, eventEmitter, network, lowLimit, highLimit, whoIsTimeout,
38
37
  name = 'discover device'
39
38
  ) {
40
39
  super();
@@ -44,7 +43,6 @@ module.exports = {
44
43
  this.lowLimit = lowLimit
45
44
  this.highLimit = highLimit
46
45
  this.whoIsTimeout = whoIsTimeout
47
- this.readDeviceNameTimeout = readDeviceNameTimeout
48
46
  this.name = name
49
47
  }
50
48
 
@@ -108,7 +106,6 @@ module.exports = {
108
106
  * @returns {Promise<void>}
109
107
  */
110
108
  async #readDevices() {
111
- const chunkSize = 100;
112
109
  const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
113
110
 
114
111
  const readDeviceName = (d, addressSet) => {
@@ -138,26 +135,22 @@ module.exports = {
138
135
  });
139
136
  };
140
137
 
141
- for (let i = 0; i < this.discoverList.length; i += chunkSize) {
142
- const chunk = this.discoverList.slice(i, i + chunkSize);
138
+ for (let i = 0; i < this.discoverList.length; i++) {
139
+ const d = this.discoverList[i];
143
140
 
144
- const promises = chunk.map(d => {
145
- let addressSet = d.address;
146
- if (d.adr !== undefined && d.net !== undefined) {
147
- addressSet = { ip: d.address, adr: d.adr, net: d.net };
148
- }
149
-
150
- return readDeviceName(d, addressSet)
151
- .then(deviceInfo => this.discoveredDevices.push(deviceInfo));
152
- });
141
+ let addressSet = d.address;
142
+ if (d.adr !== undefined && d.net !== undefined) {
143
+ addressSet = { ip: d.address, adr: d.adr, net: d.net };
144
+ }
153
145
 
154
- await Promise.allSettled(promises);
155
- await delay(this.readDeviceNameTimeout);
146
+ const deviceInfo = await readDeviceName(d, addressSet);
147
+ this.discoveredDevices.push(deviceInfo);
156
148
 
157
- // update progress
149
+ // throttle to avoid bursts
150
+ await delay(75); // tweak between 50–150ms
158
151
  this.#updateProgress(
159
- Math.min(90, Math.round((80 / this.discoverList.length) * (i + chunkSize) + 10))
160
- )
152
+ Math.min(90, Math.round((80 / this.discoverList.length) * (i + 1) + 10))
153
+ );
161
154
  }
162
155
 
163
156
  this.#updateProgress(90)
@@ -240,6 +240,8 @@ const readPoints = async (
240
240
  device, objects, eventEmitter, name, client, readMethod, maxConcurrentSinglePointRead
241
241
  ) => {
242
242
  const points = [];
243
+
244
+ // First query - get basic properties (excluding STATE_TEXT)
243
245
  const reqArr = objects.map(obj => ({
244
246
  objectId: { type: obj.value.type, instance: obj.value.instance },
245
247
  properties: [
@@ -250,38 +252,80 @@ const readPoints = async (
250
252
  { id: baEnum.PropertyIdentifier.INACTIVE_TEXT },
251
253
  { id: baEnum.PropertyIdentifier.ACTIVE_TEXT }
252
254
  ] : []),
253
- ...(multiStateObjectTypes.includes(obj.value.type) ? [{ id: baEnum.PropertyIdentifier.STATE_TEXT }] : []),
254
- // ...(scheduleObjectTypes.includes(obj.value.type) ? [
255
- // { id: baEnum.PropertyIdentifier.WEEKLY_SCHEDULE },
256
- // { id: baEnum.PropertyIdentifier.EXCEPTION_SCHEDULE },
257
- // { id: baEnum.PropertyIdentifier.SCHEDULE_DEFAULT }
258
- // ] : [])
259
255
  ]
260
256
  }));
261
257
 
262
258
  try {
259
+ // First request - get basic properties
263
260
  const result = await smartReadProperty(
264
261
  client, device, reqArr, readMethod, maxConcurrentSinglePointRead, 50
265
262
  );
263
+
264
+ // Process basic properties first
266
265
  result.forEach(i => {
267
- /** i example
268
- {
269
- objectId: { type: 1, instance: 0 },
270
- values: [
271
- { id: 85, index: 4294967295, value: [Array] },
272
- { id: 77, index: 4294967295, value: [Array] }
273
- ]
274
- }
275
- */
276
266
  const point = processPoint(i, device.deviceName);
277
267
  points.push(point);
278
268
  });
279
- return points
269
+
270
+ // Second query - get STATE_TEXT for multistate objects only
271
+ const multistateObjects = objects.filter(obj => multiStateObjectTypes.includes(obj.value.type));
272
+
273
+ if (multistateObjects.length > 0) {
274
+ const stateTextReqArr = multistateObjects.map(obj => ({
275
+ objectId: { type: obj.value.type, instance: obj.value.instance },
276
+ properties: [{ id: baEnum.PropertyIdentifier.STATE_TEXT }]
277
+ }));
278
+
279
+ try {
280
+ // Force use single read on state text to reduce loss especially long text
281
+ const stateTextResult = await smartReadProperty(
282
+ client, device, stateTextReqArr, 0, 1, 50
283
+ );
284
+
285
+ // Update points with STATE_TEXT
286
+ stateTextResult.forEach(stateTextData => {
287
+ const matchingPoint = points.find(point =>
288
+ point.bacType === stateTextData.objectId.type &&
289
+ point.bacInstance === stateTextData.objectId.instance
290
+ );
291
+
292
+ if (matchingPoint) {
293
+ updatePointWithStateText(matchingPoint, stateTextData);
294
+ }
295
+ });
296
+ } catch (stateTextError) {
297
+ // Log the error but don't fail the entire operation
298
+ eventEmitter.emit(EVENT_ERROR, errMsg(
299
+ name,
300
+ 'Warning: Failed to read STATE_TEXT properties',
301
+ stateTextError
302
+ ));
303
+ }
304
+ }
305
+
306
+ return points;
280
307
  } catch (error) {
281
308
  eventEmitter.emit(EVENT_ERROR, errMsg(name, ERR_READING_POINTS, error));
282
309
  }
283
310
  };
284
311
 
312
+ const updatePointWithStateText = (point, stateTextData) => {
313
+ const stateTextProp = stateTextData.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT);
314
+ const states = stateTextProp?.value;
315
+
316
+ if (Array.isArray(states)) {
317
+ // Update the facets with state text information
318
+ const stateTextFacet = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
319
+
320
+ // If point already has facets, append; otherwise, set new facets
321
+ if (point.facets) {
322
+ point.facets += `;${stateTextFacet}`;
323
+ } else {
324
+ point.facets = stateTextFacet;
325
+ }
326
+ }
327
+ };
328
+
285
329
  const processPoint = (i, deviceName) => {
286
330
  const point = {
287
331
  deviceName,
@@ -310,12 +354,8 @@ const setPointFacets = (point, i) => {
310
354
  const inactive = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.INACTIVE_TEXT)?.value?.[0]?.value;
311
355
  const active = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.ACTIVE_TEXT)?.value?.[0]?.value;
312
356
  point.facets = `falseText:${inactive ? inactive : 'false'};trueText:${active ? active : 'true'}`;
313
- } else if (multiStateObjectTypes.includes(point.bacType)) {
314
- const states = i.values.find(prop => prop.id === baEnum.PropertyIdentifier.STATE_TEXT)?.value;
315
- if (Array.isArray(states)) {
316
- point.facets = `range:{${states.map((item, index) => `${index + 1}:${item.value}`).join(';')}}`;
317
- }
318
357
  }
358
+ // STATE_TEXT handling removed from here - now handled separately
319
359
  };
320
360
 
321
361
  const setPointValues = (point, i) => {
@@ -12,8 +12,10 @@ const encode = (buffer, func, msgLength) => {
12
12
  exports.encode = encode;
13
13
  const decode = (buffer, offset) => {
14
14
  let len;
15
+ let linkAddress;
15
16
  const func = buffer[1];
16
17
  const msgLength = (buffer[2] << 8) | (buffer[3] << 0);
18
+
17
19
  if (buffer[0] !== baEnum.BVLL_TYPE_BACNET_IP || buffer.length !== msgLength)
18
20
  return;
19
21
  switch (func) {
@@ -25,6 +27,27 @@ const decode = (buffer, offset) => {
25
27
  break;
26
28
  case baEnum.BvlcResultPurpose.FORWARDED_NPDU:
27
29
  len = 10;
30
+ // HAL modified. Extract BVLC IP and port from buffer
31
+ if (buffer.length >= 10) {
32
+ const ipParts = [buffer[4], buffer[5], buffer[6], buffer[7]];
33
+ const port = (buffer[8] << 8) | buffer[9];
34
+
35
+ // Validate IP octets (0–255 only)
36
+ const isValidIp = ipParts.every(octet => Number.isInteger(octet) && octet >= 0 && octet <= 255);
37
+
38
+ // Validate port (1–65535; BACnet default is 47808)
39
+ const isValidPort = Number.isInteger(port) && port > 0 && port <= 65535;
40
+
41
+ // Extra IP rules
42
+ const isBroadcast = ipParts.every(octet => octet === 255); // 255.255.255.255
43
+ const isMulticast = ipParts[0] >= 224 && ipParts[0] <= 239; // 224.0.0.0 – 239.255.255.255
44
+ const isZero = ipParts.every(octet => octet === 0); // 0.0.0.0
45
+
46
+ if (isValidIp && isValidPort && !isBroadcast && !isMulticast && !isZero) {
47
+ const ip = ipParts.join(".");
48
+ linkAddress = (port === 47808) ? ip : `${ip}:${port}`;
49
+ }
50
+ }
28
51
  break;
29
52
  case baEnum.BvlcResultPurpose.REGISTER_FOREIGN_DEVICE:
30
53
  case baEnum.BvlcResultPurpose.READ_FOREIGN_DEVICE_TABLE:
@@ -41,7 +64,8 @@ const decode = (buffer, offset) => {
41
64
  return {
42
65
  len: len,
43
66
  func: func,
44
- msgLength: msgLength
67
+ msgLength: msgLength,
68
+ linkAddress: linkAddress
45
69
  };
46
70
  };
47
71
  exports.decode = decode;
@@ -458,6 +458,8 @@ class Client extends events_1.EventEmitter {
458
458
  this._handlePdu(remoteAddress, apduType, buffer, offset, msgLength, addressObject, destAddress);
459
459
  }
460
460
  _receiveData(buffer, remoteAddress) {
461
+ let srcAddress = remoteAddress
462
+
461
463
  // Check data length
462
464
  if (buffer.length < baEnum.BVLC_HEADER_LENGTH)
463
465
  return debug('Received invalid data -> Drop package');
@@ -465,9 +467,14 @@ class Client extends events_1.EventEmitter {
465
467
  const result = baBvlc.decode(buffer, 0);
466
468
  if (!result)
467
469
  return debug('Received invalid BVLC header -> Drop package');
468
- // Check BVLC function
470
+ // Check BVLC virtual link controller address if defined
469
471
  if (result.func === baEnum.BvlcResultPurpose.ORIGINAL_UNICAST_NPDU || result.func === baEnum.BvlcResultPurpose.ORIGINAL_BROADCAST_NPDU || result.func === baEnum.BvlcResultPurpose.FORWARDED_NPDU) {
470
- this._handleNpdu(buffer, result.len, buffer.length - result.len, remoteAddress);
472
+ // HAL modified. Use BACnet link address if exists
473
+ if (result.func === baEnum.BvlcResultPurpose.FORWARDED_NPDU && result.linkAddress) {
474
+ srcAddress = result.linkAddress
475
+ }
476
+
477
+ this._handleNpdu(buffer, result.len, buffer.length - result.len, srcAddress);
471
478
  }
472
479
  else {
473
480
  debug('Received unknown BVLC function -> Drop package');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halsystems/red-bacnet",
3
- "version": "1.0.23",
3
+ "version": "1.1.0",
4
4
  "description": "NodeRED BACnet IP client",
5
5
  "email": "open_source@halsystems.com.au",
6
6
  "repository": {
@@ -9,7 +9,6 @@
9
9
  lowLimit: { value: 0, required: true, validate: RED.validators.number() },
10
10
  highLimit: { value: 4194304, required: true, validate: RED.validators.number() },
11
11
  whoIsTimeout: { value: 3000, required: true, validate: RED.validators.number() },
12
- readDeviceTimeout: { value: 1000, required: true, validate: RED.validators.number() },
13
12
  },
14
13
  inputs: 1,
15
14
  outputs: 1,
@@ -46,10 +45,6 @@
46
45
  <label for="node-input-whoIsTimeout"><i class="fa fa-tag"></i> Who Is Timeout (ms)</label>
47
46
  <input type="number" id="node-input-whoIsTimeout" placeholder="3000", min="100", max="3600000">
48
47
  </div>
49
- <div class="form-row">
50
- <label for="node-input-readDeviceTimeout"><i class="fa fa-tag"></i> Read Device Timeout (ms)</label>
51
- <input type="number" id="node-input-readDeviceTimeout" placeholder="1000", min="100", max="3600000">
52
- </div>
53
48
  </script>
54
49
 
55
50
  <script type="text/html" data-help-name="discover device">
@@ -28,7 +28,6 @@ module.exports = function (RED) {
28
28
  this.lowLimit = +config.lowLimit
29
29
  this.highLimit = +config.highLimit
30
30
  this.whoIsTimeout = +config.whoIsTimeout
31
- this.readDeviceTimeout = +config.readDeviceTimeout
32
31
 
33
32
  // events
34
33
  this.#subscribeListeners();
@@ -45,7 +44,6 @@ module.exports = function (RED) {
45
44
  this.lowLimit,
46
45
  this.highLimit,
47
46
  this.whoIsTimeout,
48
- this.readDeviceTimeout
49
47
  );
50
48
  this.task.onStart();
51
49
  }
@@ -41,8 +41,7 @@
41
41
  <label for="node-input-readMethod"><i class="fa fa-list"></i> Read Method</label>
42
42
  <select id="node-input-readMethod">
43
43
  <option value="0">Read Single Only</option>
44
- <option value="1">Read Multiple Fallback Single</option>
45
- <option value="2">Comprehensive Read</option>
44
+ <option value="1">Comprehensive Read</option>
46
45
  </select>
47
46
  </div>
48
47
  <div class="form-row">
@@ -73,8 +72,7 @@
73
72
 
74
73
  <dt>readMethod<span class="property-type">number</span></dt>
75
74
  <dd> <b>Read Single Only</b>: use <code>readProperty</code></dd>
76
- <dd> <b>Read Multiple Fallback Single</b>: try <code>readPropertyMultiple</code> with consevative query size, fallback to <code>readProperty</code> if failed</dd>
77
- <dd> <b>Comprehensive Read</b>: try <code>readPropertyMultiple</code> twice with high and conservative query size, fallback to <code>readProperty</code> if failed</dd>
75
+ <dd> <b>Comprehensive Read</b>: try <code>readPropertyMultiple</code>, reduce query size if failed, and fallback to <code>readProperty</code> if query size reduced to 1</dd>
78
76
 
79
77
  <dt>groupExportDeviceCount<span class="property-type">number</span></dt>
80
78
  <dd> Emit <code>msg.payload</code> once discovered devices count reaches this limit</dd>