node-red-contrib-3dm-space 2.2.1 → 2.4.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/3dm-cloud-in.html +15 -1
- package/3dm-cloud-in.js +155 -25
- package/3dm-cloud-out.html +4 -1
- package/3dm-cloud-out.js +28 -1
- package/CHANGELOG.md +17 -0
- package/LICENSE +25 -0
- package/README.md +47 -1
- package/package.json +6 -5
package/3dm-cloud-in.html
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
server: { value: "", type: "3dm-cloud-config", required: true },
|
|
8
8
|
keyFilter: { value: "" },
|
|
9
|
-
outputMode: { value: "value" }
|
|
9
|
+
outputMode: { value: "value" },
|
|
10
|
+
subscribeLastValues: { value: false },
|
|
11
|
+
onlyChanges: { value: false }
|
|
10
12
|
},
|
|
11
13
|
inputs: 0,
|
|
12
14
|
outputs: 1,
|
|
@@ -49,6 +51,14 @@
|
|
|
49
51
|
<option value="object">Object with key</option>
|
|
50
52
|
</select>
|
|
51
53
|
</div>
|
|
54
|
+
<div class="form-row">
|
|
55
|
+
<input type="checkbox" id="node-input-subscribeLastValues" style="display:inline-block; width:auto; margin-left:105px; vertical-align:top;">
|
|
56
|
+
<label for="node-input-subscribeLastValues" style="width:70%;">Subscribe to last values</label>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="form-row">
|
|
59
|
+
<input type="checkbox" id="node-input-onlyChanges" style="display:inline-block; width:auto; margin-left:105px; vertical-align:top;">
|
|
60
|
+
<label for="node-input-onlyChanges" style="width:70%;">Only pass changed values</label>
|
|
61
|
+
</div>
|
|
52
62
|
</script>
|
|
53
63
|
|
|
54
64
|
<script type="text/html" data-help-name="3dm-cloud-in">
|
|
@@ -68,4 +78,8 @@
|
|
|
68
78
|
<li><b>Value only</b>: outputs only the selected value.</li>
|
|
69
79
|
<li><b>Object with key</b>: outputs a JSON object containing the selected key and value.</li>
|
|
70
80
|
</ul>
|
|
81
|
+
<p><b>Subscribe to last values</b> (off by default): requests saved shared attributes once on connection and on each reconnect, then continues receiving live updates. With an Attribute Key, requests that key; otherwise requests all shared attributes. This restores saved settings, not missed command history or telemetry.</p>
|
|
82
|
+
<p><b>Only pass changed values</b> (off by default): suppresses repeats of the selected key, comparing after five-decimal rounding. With no key, remembers values per attribute and suppresses a message only when every included key is unchanged. When any key changes, the complete received attribute object is passed through. Nested object key order is ignored; array order and value types matter.</p>
|
|
83
|
+
<p>The first value passes. Memory survives reconnects, but resets when this node is redeployed or Node-RED restarts. Do not enable change filtering for repeated one-shot commands that must run each time.</p>
|
|
84
|
+
<p>No polling or disk writes are added. Change memory is capped at 1,024 keys and 1 MiB of serialized key/value bytes per node (actual JavaScript heap usage is higher). Evicted or oversized values may pass again. A saved-value request times out after 15 seconds without repeated application retries; live updates remain active.</p>
|
|
71
85
|
</script>
|
package/3dm-cloud-in.js
CHANGED
|
@@ -8,6 +8,21 @@ module.exports = function (RED) {
|
|
|
8
8
|
*/
|
|
9
9
|
var ATTRIBUTES_TOPIC = "v1/devices/me/attributes";
|
|
10
10
|
var MQTT_QOS = 1;
|
|
11
|
+
var nextRequestId = 0;
|
|
12
|
+
var CACHE_MAX_KEYS = 1024;
|
|
13
|
+
var CACHE_MAX_BYTES = 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
function stableJson(value) {
|
|
16
|
+
if (Array.isArray(value)) {
|
|
17
|
+
return "[" + value.map(stableJson).join(",") + "]";
|
|
18
|
+
}
|
|
19
|
+
if (value && typeof value === "object") {
|
|
20
|
+
return "{" + Object.keys(value).sort().map(function (key) {
|
|
21
|
+
return JSON.stringify(key) + ":" + stableJson(value[key]);
|
|
22
|
+
}).join(",") + "}";
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(value);
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
function ThreeDMCloudInNode(config) {
|
|
13
28
|
RED.nodes.createNode(this, config);
|
|
@@ -18,6 +33,46 @@ module.exports = function (RED) {
|
|
|
18
33
|
node.server = RED.nodes.getNode(config.server);
|
|
19
34
|
node.keyFilter = config.keyFilter || "";
|
|
20
35
|
node.outputMode = config.outputMode || "value";
|
|
36
|
+
node.subscribeLastValues = config.subscribeLastValues === true || config.subscribeLastValues === "true";
|
|
37
|
+
node.onlyChanges = config.onlyChanges === true || config.onlyChanges === "true";
|
|
38
|
+
|
|
39
|
+
var closed = false;
|
|
40
|
+
var connected = false;
|
|
41
|
+
var generation = 0;
|
|
42
|
+
var pending = null;
|
|
43
|
+
var cache = new Map();
|
|
44
|
+
var cacheBytes = 0;
|
|
45
|
+
|
|
46
|
+
function clearPending() {
|
|
47
|
+
if (pending && pending.timer) {
|
|
48
|
+
clearTimeout(pending.timer);
|
|
49
|
+
}
|
|
50
|
+
pending = null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function changed(data) {
|
|
54
|
+
if (!node.onlyChanges) { return true; }
|
|
55
|
+
var object = data && typeof data === "object" && !Array.isArray(data);
|
|
56
|
+
var keys = node.keyFilter ? [node.keyFilter] : (object ? Object.keys(data) : [""]);
|
|
57
|
+
var anyChanged = false;
|
|
58
|
+
keys.forEach(function (key) {
|
|
59
|
+
var value = node.keyFilter || object ? data[key] : data;
|
|
60
|
+
var serialized = stableJson(value);
|
|
61
|
+
var previous = cache.get(key);
|
|
62
|
+
if (!previous || previous.value !== serialized) { anyChanged = true; }
|
|
63
|
+
if (previous) { cacheBytes -= previous.bytes; cache.delete(key); }
|
|
64
|
+
var bytes = Buffer.byteLength(key) + Buffer.byteLength(serialized);
|
|
65
|
+
if (bytes > CACHE_MAX_BYTES) { return; }
|
|
66
|
+
while (cache.size >= CACHE_MAX_KEYS || cacheBytes + bytes > CACHE_MAX_BYTES) {
|
|
67
|
+
var oldest = cache.keys().next().value;
|
|
68
|
+
cacheBytes -= cache.get(oldest).bytes;
|
|
69
|
+
cache.delete(oldest);
|
|
70
|
+
}
|
|
71
|
+
cache.set(key, { value: serialized, bytes: bytes });
|
|
72
|
+
cacheBytes += bytes;
|
|
73
|
+
});
|
|
74
|
+
return anyChanged;
|
|
75
|
+
}
|
|
21
76
|
|
|
22
77
|
if (!node.server) {
|
|
23
78
|
node.status({ fill: "red", shape: "ring", text: "missing config" });
|
|
@@ -46,17 +101,7 @@ module.exports = function (RED) {
|
|
|
46
101
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
47
102
|
}
|
|
48
103
|
|
|
49
|
-
|
|
50
|
-
updateStatus(connected);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
var messageHandler = function (topic, payload) {
|
|
54
|
-
if (topic !== ATTRIBUTES_TOPIC) {
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
var data = roundPayload(parsePayload(payload));
|
|
59
|
-
|
|
104
|
+
function outputData(data) {
|
|
60
105
|
if (node.keyFilter) {
|
|
61
106
|
if (typeof data !== "object" || data === null || Array.isArray(data)) {
|
|
62
107
|
return;
|
|
@@ -66,6 +111,8 @@ module.exports = function (RED) {
|
|
|
66
111
|
return;
|
|
67
112
|
}
|
|
68
113
|
|
|
114
|
+
if (!changed(data)) { return; }
|
|
115
|
+
|
|
69
116
|
var msg = {
|
|
70
117
|
topic: node.keyFilter,
|
|
71
118
|
key: node.keyFilter,
|
|
@@ -85,31 +132,114 @@ module.exports = function (RED) {
|
|
|
85
132
|
return;
|
|
86
133
|
}
|
|
87
134
|
|
|
135
|
+
if (!changed(data)) { return; }
|
|
88
136
|
node.send({
|
|
89
137
|
topic: "3dm.attributes",
|
|
90
138
|
payload: data
|
|
91
139
|
});
|
|
92
140
|
};
|
|
93
141
|
|
|
94
|
-
|
|
95
|
-
|
|
142
|
+
function requestLastValues(epoch) {
|
|
143
|
+
if (!node.subscribeLastValues || closed || !connected || epoch !== generation) { return; }
|
|
144
|
+
node.server.subscribe("v1/devices/me/attributes/response/+", { qos: MQTT_QOS }, function (err) {
|
|
145
|
+
if (closed || !connected || epoch !== generation) { return; }
|
|
146
|
+
if (err) {
|
|
147
|
+
node.warn("3DM Cloud saved attribute response subscribe failed: " + (err.message || err));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
var id = String(++nextRequestId);
|
|
151
|
+
pending = { id: id, touched: new Set(), touchedBytes: 0, discard: false };
|
|
152
|
+
pending.timer = setTimeout(function () {
|
|
153
|
+
clearPending();
|
|
154
|
+
if (!closed) { node.warn("3DM Cloud saved attribute request timed out; live updates remain active."); }
|
|
155
|
+
}, 15000);
|
|
156
|
+
var request = { sharedKeys: node.keyFilter };
|
|
157
|
+
node.server.publish("v1/devices/me/attributes/request/" + id, request, { qos: MQTT_QOS }, function (error) {
|
|
158
|
+
if (error && pending && pending.id === id) {
|
|
159
|
+
clearPending();
|
|
160
|
+
node.warn("3DM Cloud saved attribute request failed: " + (error.message || error));
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
96
165
|
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
166
|
+
var stateHandler = function (online) {
|
|
167
|
+
if (closed) { return; }
|
|
168
|
+
online = Boolean(online);
|
|
169
|
+
updateStatus(online);
|
|
170
|
+
if (online === connected) { return; }
|
|
171
|
+
connected = online;
|
|
172
|
+
generation += 1;
|
|
173
|
+
clearPending();
|
|
174
|
+
if (!online) { return; }
|
|
175
|
+
var epoch = generation;
|
|
176
|
+
// Subscribe to live changes first, then await the response subscription ACK.
|
|
177
|
+
node.server.subscribe(ATTRIBUTES_TOPIC, { qos: MQTT_QOS }, function (err) {
|
|
178
|
+
if (closed || !connected || epoch !== generation) { return; }
|
|
179
|
+
if (err) {
|
|
180
|
+
node.status({ fill: "red", shape: "ring", text: "subscribe failed" });
|
|
181
|
+
node.warn("3DM Cloud attribute subscribe failed: " + (err.message || err));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
requestLastValues(epoch);
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
var messageHandler = function (topic, payload) {
|
|
189
|
+
if (closed) { return; }
|
|
190
|
+
if (topic === ATTRIBUTES_TOPIC) {
|
|
191
|
+
var live = roundPayload(parsePayload(payload));
|
|
192
|
+
if (pending && live && typeof live === "object") {
|
|
193
|
+
Object.keys(live).forEach(function (key) {
|
|
194
|
+
if (pending.touched.has(key) || pending.discard) { return; }
|
|
195
|
+
var bytes = Buffer.byteLength(key);
|
|
196
|
+
if (pending.touched.size >= CACHE_MAX_KEYS || pending.touchedBytes + bytes > CACHE_MAX_BYTES) {
|
|
197
|
+
pending.discard = true;
|
|
198
|
+
} else {
|
|
199
|
+
pending.touched.add(key);
|
|
200
|
+
pending.touchedBytes += bytes;
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
outputData(live);
|
|
205
|
+
return;
|
|
103
206
|
}
|
|
104
|
-
|
|
207
|
+
if (!pending || topic !== "v1/devices/me/attributes/response/" + pending.id) { return; }
|
|
208
|
+
var response = parsePayload(payload);
|
|
209
|
+
var active = pending;
|
|
210
|
+
clearPending();
|
|
211
|
+
if (response && response.error) {
|
|
212
|
+
node.warn("3DM Cloud saved attribute response error: " + String(response.error));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (!response || typeof response !== "object" || !response.shared ||
|
|
216
|
+
typeof response.shared !== "object" || Array.isArray(response.shared) || active.discard) { return; }
|
|
217
|
+
var data = roundPayload(response.shared);
|
|
218
|
+
active.touched.forEach(function (key) { delete data[key]; });
|
|
219
|
+
if (Object.keys(data).length > 0) { outputData(data); }
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
node.server.on("state", stateHandler);
|
|
223
|
+
node.server.on("cloud-message", messageHandler);
|
|
105
224
|
|
|
106
|
-
|
|
225
|
+
// Register the live subscription even when initially offline.
|
|
226
|
+
if (node.server.connected) {
|
|
227
|
+
stateHandler(true);
|
|
228
|
+
} else {
|
|
229
|
+
node.server.subscribe(ATTRIBUTES_TOPIC, { qos: MQTT_QOS }, function (err) {
|
|
230
|
+
if (!closed && err) { node.warn("3DM Cloud attribute subscribe failed: " + (err.message || err)); }
|
|
231
|
+
});
|
|
232
|
+
updateStatus(false);
|
|
233
|
+
}
|
|
107
234
|
|
|
108
235
|
node.on("close", function () {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
236
|
+
closed = true;
|
|
237
|
+
generation += 1;
|
|
238
|
+
clearPending();
|
|
239
|
+
cache.clear();
|
|
240
|
+
cacheBytes = 0;
|
|
241
|
+
node.server.removeListener("state", stateHandler);
|
|
242
|
+
node.server.removeListener("cloud-message", messageHandler);
|
|
113
243
|
});
|
|
114
244
|
}
|
|
115
245
|
|
package/3dm-cloud-out.html
CHANGED
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
server: { value: "", type: "3dm-cloud-config", required: true }
|
|
8
8
|
},
|
|
9
9
|
inputs: 1,
|
|
10
|
-
outputs:
|
|
10
|
+
outputs: 2,
|
|
11
|
+
outputLabels: ["telemetry result", "cloud online"],
|
|
11
12
|
icon: "font-awesome/fa-cloud-upload",
|
|
12
13
|
label: function () {
|
|
13
14
|
return this.name || "3DM telemetry out";
|
|
@@ -36,6 +37,8 @@
|
|
|
36
37
|
<p>Sends telemetry to 3DM.space SCADA Cloud.</p>
|
|
37
38
|
|
|
38
39
|
<p>Input must be a JSON object.</p>
|
|
40
|
+
<p>Output 1: the existing telemetry result message.</p>
|
|
41
|
+
<p>Output 2: connection status with <code>msg.topic = "3dm.connection"</code> and a boolean <code>msg.payload</code>: <code>true</code> for online, <code>false</code> for offline. Emitted once after deployment and on each connection state change, without requiring telemetry input. A missing login configuration reports false.</p>
|
|
39
42
|
|
|
40
43
|
<p>Example:</p>
|
|
41
44
|
|
package/3dm-cloud-out.js
CHANGED
|
@@ -87,6 +87,32 @@ module.exports = function (RED) {
|
|
|
87
87
|
node.name = config.name;
|
|
88
88
|
node.server = RED.nodes.getNode(config.server);
|
|
89
89
|
|
|
90
|
+
var lastConnectionState;
|
|
91
|
+
var connectionOutputClosed = false;
|
|
92
|
+
|
|
93
|
+
function sendConnectionState(connected) {
|
|
94
|
+
connected = Boolean(connected);
|
|
95
|
+
if (connectionOutputClosed || connected === lastConnectionState) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
lastConnectionState = connected;
|
|
99
|
+
node.send([null, { topic: "3dm.connection", payload: connected }]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Defer the initial message until Node-RED has connected the flow wires.
|
|
103
|
+
var connectionStartupTimer = setTimeout(function () {
|
|
104
|
+
connectionStartupTimer = null;
|
|
105
|
+
sendConnectionState(node.server && node.server.connected);
|
|
106
|
+
}, 0);
|
|
107
|
+
|
|
108
|
+
node.on("close", function () {
|
|
109
|
+
connectionOutputClosed = true;
|
|
110
|
+
if (connectionStartupTimer) {
|
|
111
|
+
clearTimeout(connectionStartupTimer);
|
|
112
|
+
connectionStartupTimer = null;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
90
116
|
var queueFile = path.join(
|
|
91
117
|
QUEUE_DIR,
|
|
92
118
|
QUEUE_FILE_PREFIX + "-" + node.id + ".sqlite"
|
|
@@ -296,7 +322,7 @@ module.exports = function (RED) {
|
|
|
296
322
|
} else {
|
|
297
323
|
msg.payload = roundPayload(msg.payload);
|
|
298
324
|
}
|
|
299
|
-
send(msg);
|
|
325
|
+
send([msg, null]);
|
|
300
326
|
|
|
301
327
|
if (done) {
|
|
302
328
|
done();
|
|
@@ -605,6 +631,7 @@ module.exports = function (RED) {
|
|
|
605
631
|
}
|
|
606
632
|
|
|
607
633
|
var stateHandler = function (connected) {
|
|
634
|
+
sendConnectionState(connected);
|
|
608
635
|
if (connected) {
|
|
609
636
|
updateStatus("connected");
|
|
610
637
|
maybeStartBackhaul();
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.4.0 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
- Add optional Subscribe to last values to 3DM In for initial connect and reconnect recovery of saved shared attributes.
|
|
6
|
+
- Add optional Only pass changed values, retaining a bounded per-node comparison cache across communications disconnects.
|
|
7
|
+
- Keep both options off by default and preserve existing output formats, five-decimal precision, cloud-out status pin, and Smithtek proprietary licence.
|
|
8
|
+
- Correlate response IDs, wait for subscription acknowledgement, ignore obsolete responses, protect newer live values, and clear request timers on disconnect/close.
|
|
9
|
+
- Document memory limits, local cache measurements, server/network impact, and command-filtering behaviour.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## 2.3.0 — 2026-10-18
|
|
13
|
+
|
|
14
|
+
- Add a second 3DM Out output for boolean cloud connection status: true online, false offline, on deployment and state changes.
|
|
15
|
+
- Preserve existing telemetry results on output 1, queue behavior, rate limiting, and five-decimal payload precision.
|
|
16
|
+
- Replace GPL declarations with Smithtek proprietary licence terms and include LICENSE in the release.
|
|
17
|
+
- Preserve all 2.2.1 documentation and assets; extend the npm file list to include all Markdown documents.
|
|
18
|
+
|
|
19
|
+
|
|
3
20
|
## 2.2.1 — 2026-10-01
|
|
4
21
|
|
|
5
22
|
- Round numeric output payload values to a maximum of five decimal places, including nested objects and arrays.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Smithtek Proprietary Software Licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Smithtek. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software, including its source code, documentation, and accompanying
|
|
6
|
+
Smithtek materials, is proprietary to Smithtek. It is not licensed under the
|
|
7
|
+
GPL or any other open-source licence.
|
|
8
|
+
|
|
9
|
+
Possession, download, installation, or public availability of this package does
|
|
10
|
+
not grant a licence. Use is permitted only under a separate written agreement
|
|
11
|
+
or express written permission from Smithtek, and only within its scope.
|
|
12
|
+
|
|
13
|
+
Without that permission, no person may use, copy, modify, distribute,
|
|
14
|
+
sublicense, sell, or create derivative works of this software, except to the
|
|
15
|
+
extent applicable law requires otherwise. Copyright and proprietary notices
|
|
16
|
+
must be retained in any authorised copies.
|
|
17
|
+
|
|
18
|
+
Third-party dependencies and materials remain subject to their respective
|
|
19
|
+
licences. These terms do not replace or restrict rights granted by those
|
|
20
|
+
third-party licences.
|
|
21
|
+
|
|
22
|
+
To the extent permitted by law, this software is provided AS IS, without
|
|
23
|
+
warranties of any kind. Smithtek shall not be liable for loss or damage arising
|
|
24
|
+
from use of the software. A separate written agreement with Smithtek takes
|
|
25
|
+
precedence over these terms where they conflict.
|
package/README.md
CHANGED
|
@@ -185,6 +185,15 @@ If the cloud connection is unavailable, valid data is stored locally and sent la
|
|
|
185
185
|
|
|
186
186
|
---
|
|
187
187
|
|
|
188
|
+
## 3DM Out Outputs
|
|
189
|
+
|
|
190
|
+
From version 2.3.0, 3DM Out has two output pins:
|
|
191
|
+
|
|
192
|
+
1. **Telemetry result**: the existing result message, including sent, queued, rate-limit, and error fields.
|
|
193
|
+
2. **Cloud online**: a message with boolean `msg.payload`: `true` when the shared MQTT connection to 3DM.space is online, or `false` when offline. The topic is `3dm.connection`.
|
|
194
|
+
|
|
195
|
+
The second output emits once after deployment and whenever the connection state changes. It works without incoming telemetry. Repeated notifications of the same state do not produce duplicate messages. A missing cloud login configuration reports `false`. This reports MQTT connection status; it does not confirm that an individual telemetry packet was delivered or stored. Existing wires on output 1 keep their telemetry behavior. Connect output 2 to a Switch node or other flow logic to react to online/offline changes.
|
|
196
|
+
|
|
188
197
|
## 3DM Out Input Data Type
|
|
189
198
|
|
|
190
199
|
The 3DM Out node expects `msg.payload` to be a JSON object.
|
|
@@ -568,6 +577,41 @@ Use this for:
|
|
|
568
577
|
|
|
569
578
|
---
|
|
570
579
|
|
|
580
|
+
## 3DM In Saved Values and Change Filtering
|
|
581
|
+
|
|
582
|
+
Version 2.4.0 adds two independent checkboxes. Both default to **off**, preserving existing flows.
|
|
583
|
+
|
|
584
|
+
### Subscribe to last values
|
|
585
|
+
|
|
586
|
+
When enabled, the node requests the current saved **shared attributes** on initial connection and on every reconnect. With an Attribute Key selected, it requests that key only. With no key, it requests all shared attributes. Restored values use the existing Output Type and five-decimal precision settings, then normal live updates continue.
|
|
587
|
+
|
|
588
|
+
This reads saved state, not historical telemetry or a replay of every missed command. The response subscription is acknowledged before the request is published. Requests have unique IDs so nodes sharing a login only handle their own replies. A 15-second response timeout clears the pending request and logs a warning; live updates continue. There is no periodic polling or repeated application retry loop. The next reconnect requests again. A live value received while awaiting a snapshot takes precedence over that snapshot for the same key. Temporary live-key tracking is bounded to 1,024 keys and 1 MiB of key-name bytes. If that limit is exceeded before the response, the snapshot is discarded to avoid overwriting newer live values.
|
|
589
|
+
|
|
590
|
+
### Only pass changed values
|
|
591
|
+
|
|
592
|
+
When enabled, the first observed value passes. Later messages pass only when the selected key changes, including values restored after reconnect. Values are compared after rounding to five decimal places. Types matter (for example, false and 0 differ). Nested object property order is ignored; array order matters.
|
|
593
|
+
|
|
594
|
+
With no Attribute Key, the node remembers each received attribute separately. A message is suppressed only if every included key is unchanged. When any key is new or changed, the complete received attribute object passes, including any unchanged keys in that message. Missing keys in a partial update are not interpreted as deleted attributes.
|
|
595
|
+
|
|
596
|
+
The cache survives a communications disconnect. It resets when this node is recreated by a deployment or Node-RED restarts, so the first value then passes again. Do not enable this option for repeated one-shot commands that must execute each time even if the value is identical. Comparing values does not provide command execution acknowledgement.
|
|
597
|
+
|
|
598
|
+
### Node-RED memory and CPU impact
|
|
599
|
+
|
|
600
|
+
- No new MQTT connection, disk storage, persistent history, or polling loop is added.
|
|
601
|
+
- With change filtering off, no previous attribute values are retained. Saved-value fetching holds one pending request ID, one timeout, and temporary tracking of live keys while awaiting its reply.
|
|
602
|
+
- With change filtering on, each node retains only its latest comparison value per relevant attribute. The cache is limited to **1,024 keys** and **1 MiB of serialized UTF-8 key/value bytes**, whichever is reached first. Actual JavaScript heap use is higher because of strings, maps, and object overhead; the 1 MiB limit is not a process-memory limit. Parsing, rounding, and comparison also allocate temporary memory proportional to incoming payload size.
|
|
603
|
+
- Oversized values are passed without being cached. Least-recently-used keys are evicted to stay within the limits. These values may therefore pass again even if unchanged.
|
|
604
|
+
- Synthetic Node.js v22.17.0 tests measured roughly **0.1 KiB** additional retained heap for populating one boolean cache entry, **10 KiB** for one 10 KiB string, and **9.4 KiB** for 100 numeric attributes. These are approximate cache-population measurements after node creation, excluding the base node, empty cache, Node-RED runtime, MQTT connection, and transient request/payload allocations. Gateway hardware and Node.js versions will differ.
|
|
605
|
+
- Comparison work grows with the size of the value. Nested object keys are sorted for consistent comparison. For ordinary booleans and numeric setpoints, little data needs comparing; large configuration objects cost more. No production CPU or latency benchmark has been performed.
|
|
606
|
+
|
|
607
|
+
### 3DM.space server and network impact
|
|
608
|
+
|
|
609
|
+
Each enabled 3DM In node sends **one attribute read request per successful connection/reconnect**, receives one response, and subscribes to the response topic on the existing MQTT connection. MQTT acknowledgements and subscription traffic add protocol overhead. If ten enabled In nodes share a login, one reconnect causes ten read requests and normally ten responses; requests are not combined between nodes. Repeated connection failures can increase this traffic, and large all-attribute responses use more bandwidth than a selected-key request.
|
|
610
|
+
|
|
611
|
+
The feature reads attributes; it does not write telemetry or configuration. Server processing includes request handling, attribute retrieval, and response serialization. Exact server RAM/CPU use depends on the deployed ThingsBoard version, attribute sizes, device count, and cache/database configuration; it has not been measured on 3DM.space. Change filtering runs locally **after receipt**, so it reduces downstream Node-RED work but does not reduce the server's live attribute notifications or MQTT bandwidth. Prefer a selected Attribute Key when only one value is needed.
|
|
612
|
+
|
|
613
|
+
Implementation follows the [ThingsBoard MQTT attribute request API](https://thingsboard.io/docs/pe/reference/mqtt-api/attributes/). Local simulated MQTT tests verify the request/response flow; a live reconnect test against your 3DM.space instance is still required to confirm deployment-specific behaviour.
|
|
614
|
+
|
|
571
615
|
## 3DM In Status Messages
|
|
572
616
|
|
|
573
617
|
### connected
|
|
@@ -2613,4 +2657,6 @@ Smithtek
|
|
|
2613
2657
|
|
|
2614
2658
|
# License
|
|
2615
2659
|
|
|
2616
|
-
|
|
2660
|
+
Smithtek proprietary software. Copyright (c) 2026 Smithtek. All rights reserved.
|
|
2661
|
+
|
|
2662
|
+
This package is not offered under an open-source licence. Use requires permission from Smithtek. See [LICENSE](LICENSE) for the proprietary terms. Third-party dependencies retain their own licences.
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-3dm-space",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Node-RED nodes for 3DM.space SCADA Cloud telemetry, attributes, and local config storage",
|
|
5
5
|
"main": "3dm-cloud-config.js",
|
|
6
|
-
"license": "
|
|
6
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"keywords": [
|
|
8
8
|
"node-red",
|
|
9
9
|
"node-red-contrib",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"payload-precision.js",
|
|
38
38
|
"icons/",
|
|
39
39
|
"icon.png",
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
]
|
|
40
|
+
"*.md",
|
|
41
|
+
"LICENSE"
|
|
42
|
+
],
|
|
43
|
+
"author": "Smithtek"
|
|
43
44
|
}
|