node-red-contrib-uos-nats 0.2.48 → 0.2.49
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/nodes/datahub-output.js +140 -158
- package/package.json +1 -1
package/nodes/datahub-output.js
CHANGED
|
@@ -131,7 +131,7 @@ module.exports = function (RED) {
|
|
|
131
131
|
|
|
132
132
|
// If we have no definitions yet, nothing to send
|
|
133
133
|
if (definitions.length === 0) {
|
|
134
|
-
console.log('[DataHub Output] Heartbeat skipped: No definitions.');
|
|
134
|
+
// console.log('[DataHub Output] Heartbeat skipped: No definitions.');
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -142,174 +142,156 @@ module.exports = function (RED) {
|
|
|
142
142
|
stateObj[s.id] = s;
|
|
143
143
|
}
|
|
144
144
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
145
|
+
sendValuesUpdate();
|
|
146
|
+
}, 1000); // 1.0s interval matches Python SDK
|
|
147
|
+
|
|
148
|
+
const start = async () => {
|
|
149
|
+
try {
|
|
150
|
+
console.log('[DataHub Output] Starting...');
|
|
151
|
+
this.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
|
|
152
|
+
const [payloads, subjects] = await loadModules();
|
|
153
|
+
console.log('[DataHub Output] Modules loaded.');
|
|
154
|
+
loadedPayloads = payloads;
|
|
155
|
+
loadedSubjects = subjects;
|
|
156
|
+
|
|
157
|
+
nc = await connection.acquire();
|
|
158
|
+
console.log('[DataHub Output] NATS acquired.');
|
|
159
|
+
|
|
160
|
+
await sendDefinitionUpdate(payloads, subjects);
|
|
161
|
+
|
|
162
|
+
// Listen for Variable READ requests
|
|
163
|
+
sub = nc.subscribe(subjects.readVariablesQuery(this.providerId), {
|
|
164
|
+
callback: (err, msg) => {
|
|
165
|
+
if (err) {
|
|
166
|
+
this.warn(`Read request error: ${err.message}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
handleRead(payloads, msg).catch((error) => this.warn(error.message));
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
console.log('[DataHub Output] Subscribed to Read Query.');
|
|
173
|
+
|
|
174
|
+
// Listen for Definition READ requests (Discovery)
|
|
175
|
+
// SKIPPED: Permission Violation on v1.loc.<id>.def.qry.read
|
|
176
|
+
// Data Hub seems to discover providers via initial announcement or direct variable reads.
|
|
177
|
+
/*
|
|
178
|
+
const defSub = nc.subscribe(subjects.readProviderDefinitionQuery(this.providerId), {
|
|
179
|
+
callback: (err, msg) => {
|
|
180
|
+
if (err) {
|
|
181
|
+
this.warn(`Def request error: ${err.message}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (!msg.reply) return;
|
|
185
|
+
|
|
186
|
+
// Send known definition
|
|
187
|
+
const { payload } = payloads.buildProviderDefinitionEvent(definitions);
|
|
188
|
+
nc.publish(msg.reply, payload);
|
|
186
189
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
190
|
+
});
|
|
191
|
+
*/
|
|
192
|
+
|
|
193
|
+
// Track the subscription to close it later if needed (though existing code only tracks 'sub')
|
|
194
|
+
// Ideally we should track both or use a subscription manager, but for now let's hope 'sub' isn't the only one closed.
|
|
195
|
+
// Actually, looking at close(), it likely calls connection.release(). NATS connection close cleans up subs.
|
|
196
|
+
|
|
197
|
+
this.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
198
|
+
|
|
199
|
+
// Heartbeat Removed: Periodic republishing causes UI flickering/refresh in DataHub.
|
|
200
|
+
// The definition should only be sent on start or when it actually changes.
|
|
201
|
+
/*
|
|
202
|
+
const outputHeartbeat = setInterval(() => {
|
|
203
|
+
if (nc && !nc.isClosed()) {
|
|
204
|
+
sendDefinitionUpdate(payloads, subjects).catch(err => {
|
|
205
|
+
this.warn(`Heartbeat error: ${err.message}`);
|
|
206
|
+
});
|
|
201
207
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
216
|
-
|
|
217
|
-
// Heartbeat Removed: Periodic republishing causes UI flickering/refresh in DataHub.
|
|
218
|
-
// The definition should only be sent on start or when it actually changes.
|
|
219
|
-
/*
|
|
220
|
-
const outputHeartbeat = setInterval(() => {
|
|
221
|
-
if (nc && !nc.isClosed()) {
|
|
222
|
-
sendDefinitionUpdate(payloads, subjects).catch(err => {
|
|
223
|
-
this.warn(`Heartbeat error: ${err.message}`);
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
}, 10000); // Every 10 seconds
|
|
227
|
-
*/
|
|
228
|
-
|
|
229
|
-
this.on('input', async (msg, send, done) => {
|
|
230
|
-
try {
|
|
231
|
-
// Auto-parse string payloads
|
|
232
|
-
if (typeof msg.payload === 'string') {
|
|
233
|
-
try {
|
|
234
|
-
msg.payload = JSON.parse(msg.payload);
|
|
235
|
-
} catch (e) {
|
|
236
|
-
// Ignore parse error, let validation below handle it
|
|
208
|
+
}, 10000); // Every 10 seconds
|
|
209
|
+
*/
|
|
210
|
+
|
|
211
|
+
this.on('input', async (msg, send, done) => {
|
|
212
|
+
try {
|
|
213
|
+
// Auto-parse string payloads
|
|
214
|
+
if (typeof msg.payload === 'string') {
|
|
215
|
+
try {
|
|
216
|
+
msg.payload = JSON.parse(msg.payload);
|
|
217
|
+
} catch (e) {
|
|
218
|
+
// Ignore parse error, let validation below handle it
|
|
219
|
+
}
|
|
237
220
|
}
|
|
238
|
-
}
|
|
239
221
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
222
|
+
if (!msg || !msg.payload || typeof msg.payload !== 'object') {
|
|
223
|
+
done(new Error('Payload must be an object describing your structure.'));
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const entries = flattenPayload(msg.payload);
|
|
245
227
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
228
|
+
// Optimization: If payload is empty after flattening (e.g. only undefined values), stop here
|
|
229
|
+
if (!entries.length) {
|
|
230
|
+
done();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
251
233
|
|
|
252
|
-
|
|
253
|
-
|
|
234
|
+
let definitionsChanged = false;
|
|
235
|
+
const states = [];
|
|
236
|
+
|
|
237
|
+
entries.forEach(({ key, value }) => {
|
|
238
|
+
// Ensure we don't accidentally send undefined/null as value if logic slipped through
|
|
239
|
+
if (value === undefined || value === null) return;
|
|
240
|
+
|
|
241
|
+
const { def, created } = ensureDefinition(key, inferType(value));
|
|
242
|
+
if (created) {
|
|
243
|
+
definitionsChanged = true;
|
|
244
|
+
}
|
|
245
|
+
const state = {
|
|
246
|
+
id: def.id,
|
|
247
|
+
value,
|
|
248
|
+
timestamp: BigInt(Date.now()) * 1_000_000n,
|
|
249
|
+
quality: 'GOOD',
|
|
250
|
+
};
|
|
251
|
+
// states.push(state); // No longer pushing to a temporary 'states' array
|
|
252
|
+
stateMap.set(def.id, state); // Update the global stateMap
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (definitionsChanged) {
|
|
256
|
+
// If definition changed, we MUST publish definition first
|
|
257
|
+
await sendDefinitionUpdate(loadedPayloads, loadedSubjects);
|
|
258
|
+
await new Promise(r => setTimeout(r, 200));
|
|
259
|
+
}
|
|
254
260
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (value === undefined || value === null) return;
|
|
261
|
+
// Publish values immediately on input (don't wait for heartbeat)
|
|
262
|
+
await sendValuesUpdate();
|
|
258
263
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
id: def.id,
|
|
265
|
-
value,
|
|
266
|
-
timestamp: BigInt(Date.now()) * 1_000_000n,
|
|
267
|
-
quality: 'GOOD',
|
|
268
|
-
};
|
|
269
|
-
// states.push(state); // No longer pushing to a temporary 'states' array
|
|
270
|
-
stateMap.set(def.id, state); // Update the global stateMap
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
if (definitionsChanged) {
|
|
274
|
-
// If definition changed, we MUST publish definition first
|
|
275
|
-
await sendDefinitionUpdate(loadedPayloads, loadedSubjects);
|
|
276
|
-
await new Promise(r => setTimeout(r, 200));
|
|
264
|
+
send(msg);
|
|
265
|
+
done();
|
|
266
|
+
}
|
|
267
|
+
catch (err) {
|
|
268
|
+
done(err);
|
|
277
269
|
}
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
this.status({ fill: 'red', shape: 'ring', text: err.message });
|
|
274
|
+
this.error(err.message);
|
|
275
|
+
}
|
|
276
|
+
};
|
|
278
277
|
|
|
279
|
-
|
|
280
|
-
await sendValuesUpdate();
|
|
278
|
+
start();
|
|
281
279
|
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
this.on('close', async (done) => {
|
|
281
|
+
try {
|
|
282
|
+
if (valueHeartbeat) clearInterval(valueHeartbeat);
|
|
283
|
+
if (sub) {
|
|
284
|
+
await sub.drain();
|
|
284
285
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
catch (err) {
|
|
291
|
-
this.status({ fill: 'red', shape: 'ring', text: err.message });
|
|
292
|
-
this.error(err.message);
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
start();
|
|
297
|
-
|
|
298
|
-
this.on('close', async (done) => {
|
|
299
|
-
try {
|
|
300
|
-
if (valueHeartbeat) clearInterval(valueHeartbeat);
|
|
301
|
-
if (sub) {
|
|
302
|
-
await sub.drain();
|
|
286
|
+
// if (outputHeartbeat) clearInterval(outputHeartbeat);
|
|
287
|
+
await connection.release();
|
|
303
288
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
done();
|
|
311
|
-
});
|
|
312
|
-
}
|
|
289
|
+
catch (err) {
|
|
290
|
+
this.warn(`closing error: ${err.message}`);
|
|
291
|
+
}
|
|
292
|
+
done();
|
|
293
|
+
});
|
|
294
|
+
}
|
|
313
295
|
|
|
314
|
-
|
|
315
|
-
};
|
|
296
|
+
RED.nodes.registerType('datahub-output', DataHubOutputNode);
|
|
297
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.49",
|
|
4
4
|
"description": "Node-RED nodes for Weidmüller u-OS Data Hub. Read, write, and provide variables via NATS protocol with OAuth2 authentication. Features: Variable Key resolution, custom icons, example flows, and provider definition caching.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "IoTUeli",
|