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.
@@ -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
- const payload = loadedPayloads.buildVariablesChangedEvent(definitions, stateObj, fingerprint);
146
-
147
- // Debug: Log complete HEX dump to verify flatbuffer structure
148
- const hex = Buffer.from(payload).toString('hex');
149
-
150
- const subject = loadedSubjects.varsChangedEvent(this.providerId);
151
- console.log(`[DataHub Output] Sending Heartbeat to '${subject}'`);
152
- console.log(`[DataHub Output] Packet HEX (${payload.length} bytes): ${hex}`);
153
-
154
- await nc.publish(subject, payload);
155
- await nc.flush(); // Ensure NATS accepts the packet (catches Permission Errors)
156
- console.log(`[DataHub Output] Heartbeat sent. State count: ${Object.keys(stateObj).length}`);
157
- } catch (err) {
158
- this.warn(`Heartbeat error: ${err.message}`);
159
- }
160
- };
161
-
162
- const valueHeartbeat = setInterval(() => {
163
- sendValuesUpdate();
164
- }, 1000); // 1.0s interval matches Python SDK
165
-
166
- const start = async () => {
167
- try {
168
- console.log('[DataHub Output] Starting...');
169
- this.status({ fill: 'yellow', shape: 'ring', text: 'connecting…' });
170
- const [payloads, subjects] = await loadModules();
171
- console.log('[DataHub Output] Modules loaded.');
172
- loadedPayloads = payloads;
173
- loadedSubjects = subjects;
174
-
175
- nc = await connection.acquire();
176
- console.log('[DataHub Output] NATS acquired.');
177
-
178
- await sendDefinitionUpdate(payloads, subjects);
179
-
180
- // Listen for Variable READ requests
181
- sub = nc.subscribe(subjects.readVariablesQuery(this.providerId), {
182
- callback: (err, msg) => {
183
- if (err) {
184
- this.warn(`Read request error: ${err.message}`);
185
- return;
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
- handleRead(payloads, msg).catch((error) => this.warn(error.message));
188
- },
189
- });
190
- console.log('[DataHub Output] Subscribed to Read Query.');
191
-
192
- // Listen for Definition READ requests (Discovery)
193
- // SKIPPED: Permission Violation on v1.loc.<id>.def.qry.read
194
- // Data Hub seems to discover providers via initial announcement or direct variable reads.
195
- /*
196
- const defSub = nc.subscribe(subjects.readProviderDefinitionQuery(this.providerId), {
197
- callback: (err, msg) => {
198
- if (err) {
199
- this.warn(`Def request error: ${err.message}`);
200
- return;
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
- if (!msg.reply) return;
203
-
204
- // Send known definition
205
- const { payload } = payloads.buildProviderDefinitionEvent(definitions);
206
- nc.publish(msg.reply, payload);
207
- }
208
- });
209
- */
210
-
211
- // Track the subscription to close it later if needed (though existing code only tracks 'sub')
212
- // Ideally we should track both or use a subscription manager, but for now let's hope 'sub' isn't the only one closed.
213
- // Actually, looking at close(), it likely calls connection.release(). NATS connection close cleans up subs.
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
- if (!msg || !msg.payload || typeof msg.payload !== 'object') {
241
- done(new Error('Payload must be an object describing your structure.'));
242
- return;
243
- }
244
- const entries = flattenPayload(msg.payload);
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
- // Optimization: If payload is empty after flattening (e.g. only undefined values), stop here
247
- if (!entries.length) {
248
- done();
249
- return;
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
- let definitionsChanged = false;
253
- const states = [];
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
- entries.forEach(({ key, value }) => {
256
- // Ensure we don't accidentally send undefined/null as value if logic slipped through
257
- if (value === undefined || value === null) return;
261
+ // Publish values immediately on input (don't wait for heartbeat)
262
+ await sendValuesUpdate();
258
263
 
259
- const { def, created } = ensureDefinition(key, inferType(value));
260
- if (created) {
261
- definitionsChanged = true;
262
- }
263
- const state = {
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
- // Publish values immediately on input (don't wait for heartbeat)
280
- await sendValuesUpdate();
278
+ start();
281
279
 
282
- send(msg);
283
- done();
280
+ this.on('close', async (done) => {
281
+ try {
282
+ if (valueHeartbeat) clearInterval(valueHeartbeat);
283
+ if (sub) {
284
+ await sub.drain();
284
285
  }
285
- catch (err) {
286
- done(err);
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
- // if (outputHeartbeat) clearInterval(outputHeartbeat);
305
- await connection.release();
306
- }
307
- catch (err) {
308
- this.warn(`closing error: ${err.message}`);
309
- }
310
- done();
311
- });
312
- }
289
+ catch (err) {
290
+ this.warn(`closing error: ${err.message}`);
291
+ }
292
+ done();
293
+ });
294
+ }
313
295
 
314
- RED.nodes.registerType('datahub-output', DataHubOutputNode);
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.48",
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",