node-red-contrib-uos-nats 1.0.1 → 1.0.3
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/lib/provider.js +23 -3
- package/nodes/datahub-output.js +5 -5
- package/nodes/datahub-write.html +54 -32
- package/package.json +1 -1
package/lib/provider.js
CHANGED
|
@@ -2,8 +2,8 @@ import { connect } from 'nats';
|
|
|
2
2
|
import { requestToken } from './auth.js';
|
|
3
3
|
import { CLIENT_NAME, NATS_SERVER, PROVIDER_ID, PUBLISH_INTERVAL_MS, VARIABLE_DEFINITIONS, } from './config.js';
|
|
4
4
|
import { SimulationEngine } from './simulation.js';
|
|
5
|
-
import { buildProviderDefinitionEvent, buildReadVariablesResponse, buildVariablesChangedEvent, } from './payloads.js';
|
|
6
|
-
import { providerDefinitionChanged, readVariablesQuery, varsChangedEvent } from './subjects.js';
|
|
5
|
+
import { buildProviderDefinitionEvent, buildReadVariablesResponse, buildVariablesChangedEvent, parseRegistryStateEvent, } from './payloads.js';
|
|
6
|
+
import { providerDefinitionChanged, readVariablesQuery, varsChangedEvent, registryStateEvent } from './subjects.js';
|
|
7
7
|
async function main() {
|
|
8
8
|
console.log(`[provider] Node Provider für ${PROVIDER_ID} wird gestartet…`);
|
|
9
9
|
const token = await requestToken();
|
|
@@ -15,8 +15,28 @@ async function main() {
|
|
|
15
15
|
});
|
|
16
16
|
console.log('[provider] NATS verbunden:', nc.getServer());
|
|
17
17
|
const simulation = new SimulationEngine(VARIABLE_DEFINITIONS);
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
// Initial Publish
|
|
20
|
+
let { payload: definitionPayload, fingerprint } = buildProviderDefinitionEvent(VARIABLE_DEFINITIONS);
|
|
19
21
|
await nc.publish(providerDefinitionChanged(PROVIDER_ID), definitionPayload);
|
|
22
|
+
|
|
23
|
+
// Registry State Listener (Keep Alive)
|
|
24
|
+
nc.subscribe(registryStateEvent(), {
|
|
25
|
+
callback: async (err, msg) => {
|
|
26
|
+
if (err) return;
|
|
27
|
+
try {
|
|
28
|
+
const state = parseRegistryStateEvent(msg.data);
|
|
29
|
+
if (state === 1) { // 1 = RUNNING
|
|
30
|
+
console.log('[provider] Registry restart detected. Re-publishing definition...');
|
|
31
|
+
const { payload: newPayload } = buildProviderDefinitionEvent(VARIABLE_DEFINITIONS);
|
|
32
|
+
await nc.publish(providerDefinitionChanged(PROVIDER_ID), newPayload);
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
console.error('[provider] Error handling registry state:', e);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
20
40
|
const readSubject = readVariablesQuery(PROVIDER_ID);
|
|
21
41
|
const sub = nc.subscribe(readSubject, {
|
|
22
42
|
callback: async (err, msg) => {
|
package/nodes/datahub-output.js
CHANGED
|
@@ -276,17 +276,17 @@ module.exports = function (RED) {
|
|
|
276
276
|
|
|
277
277
|
this.status({ fill: 'green', shape: 'dot', text: 'ready' });
|
|
278
278
|
|
|
279
|
-
// Heartbeat
|
|
280
|
-
//
|
|
281
|
-
/*
|
|
279
|
+
// Heartbeat Restored: Provider disappears after ~20 minutes if definition is not refreshed.
|
|
280
|
+
// Set to 15 minutes (900000ms) to avoid frequent UI flickering but ensure persistence.
|
|
282
281
|
const outputHeartbeat = setInterval(() => {
|
|
283
282
|
if (nc && !nc.isClosed()) {
|
|
283
|
+
console.log('[DataHub Output] Sending Definition Heartbeat...');
|
|
284
284
|
sendDefinitionUpdate(payloads, subjects).catch(err => {
|
|
285
285
|
this.warn(`Heartbeat error: ${err.message}`);
|
|
286
286
|
});
|
|
287
287
|
}
|
|
288
|
-
},
|
|
289
|
-
|
|
288
|
+
}, 900000);
|
|
289
|
+
|
|
290
290
|
|
|
291
291
|
this.on('input', async (msg, send, done) => {
|
|
292
292
|
try {
|
package/nodes/datahub-write.html
CHANGED
|
@@ -28,13 +28,12 @@
|
|
|
28
28
|
const $inputId = $('#node-input-variableId');
|
|
29
29
|
const $inputKey = $('#node-input-variableKey');
|
|
30
30
|
|
|
31
|
-
// Styles matching Read Node
|
|
32
|
-
// Was: 60px 1fr 80px 80px
|
|
33
|
-
// New: 30px 60px 1fr
|
|
31
|
+
// Styles matching Read Node exactly
|
|
34
32
|
const rowStyle = "display:grid; grid-template-columns: 30px 60px 1fr; align-items:center; padding:4px 0; border-bottom:1px solid #eee; font-family:'Helvetica Neue', Arial, sans-serif; font-size:12px; cursor:pointer;";
|
|
35
33
|
const idStyle = "background:#eee; color:#555; padding:1px 4px; border-radius:3px; font-family:monospace; text-align:center; font-size:11px;";
|
|
36
34
|
const keyStyle = "overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding-left:10px;";
|
|
37
|
-
|
|
35
|
+
|
|
36
|
+
// Removed iconStyle, using checkbox instead
|
|
38
37
|
|
|
39
38
|
let currentVariables = [];
|
|
40
39
|
|
|
@@ -48,53 +47,76 @@
|
|
|
48
47
|
|
|
49
48
|
vars.forEach(v => {
|
|
50
49
|
const safeId = (v.id !== undefined) ? v.id : (v.Id !== undefined ? v.Id : 'ERR');
|
|
51
|
-
// Type/Access removed from display as per request
|
|
52
|
-
|
|
53
50
|
const isSelected = String(safeId) === String($inputId.val());
|
|
54
51
|
|
|
55
52
|
const row = $('<div>', { class: 'var-row', style: rowStyle });
|
|
56
|
-
if (isSelected) {
|
|
57
|
-
row.css('background', '#dcefff');
|
|
58
|
-
}
|
|
59
53
|
|
|
60
|
-
//
|
|
61
|
-
const
|
|
54
|
+
// Checkbox Container
|
|
55
|
+
const cbContainer = $('<div>', { style: 'text-align:center;' });
|
|
56
|
+
// Use actual checkbox to match Read Node visual
|
|
57
|
+
const cb = $('<input type="checkbox" class="var-checkbox">')
|
|
58
|
+
.prop('checked', isSelected)
|
|
59
|
+
.css('cursor', 'pointer');
|
|
60
|
+
|
|
61
|
+
cbContainer.append(cb);
|
|
62
62
|
|
|
63
63
|
// ID
|
|
64
64
|
const idCol = $('<div>', { style: 'text-align:center;' }).append($('<span>', { style: idStyle }).text(safeId));
|
|
65
65
|
// Key
|
|
66
66
|
const keyCol = $('<div>', { style: keyStyle, title: v.key }).text(v.key);
|
|
67
67
|
|
|
68
|
-
row.append(
|
|
69
|
-
|
|
70
|
-
// Click Handler - Fill Inputs
|
|
71
|
-
row.on('click', function () {
|
|
72
|
-
// Reset all rows
|
|
73
|
-
$('.var-row').css('background', 'transparent');
|
|
74
|
-
$('.var-row i.fa').removeClass('fa-dot-circle-o').addClass('fa-circle-o').css('color', '#ccc');
|
|
68
|
+
row.append(cbContainer).append(idCol).append(keyCol);
|
|
75
69
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
// Click Handler (Row or Checkbox)
|
|
71
|
+
const selectRow = () => {
|
|
72
|
+
// Uncheck all others (Single Force)
|
|
73
|
+
$('.var-checkbox').prop('checked', false);
|
|
74
|
+
cb.prop('checked', true);
|
|
79
75
|
|
|
80
76
|
$inputId.val(safeId !== 'ERR' ? safeId : '');
|
|
81
77
|
$inputKey.val(v.key);
|
|
82
|
-
|
|
83
|
-
// Trigger validation visual update if needed
|
|
84
78
|
$inputId.trigger('change');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Toggle behavior if clicking already selected?
|
|
82
|
+
// Usually for single select, clicking selected does nothing or keeps it selected.
|
|
83
|
+
// If user clicks checkbox directly:
|
|
84
|
+
cb.on('click', function (e) {
|
|
85
|
+
e.stopPropagation(); // prevent row click double bubble
|
|
86
|
+
if ($(this).prop('checked')) {
|
|
87
|
+
selectRow();
|
|
88
|
+
} else {
|
|
89
|
+
// Allowing deserialize? imply batch mode if none selected
|
|
90
|
+
$inputId.val('');
|
|
91
|
+
$inputKey.val('');
|
|
92
|
+
$inputId.trigger('change');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
row.on('click', function () {
|
|
97
|
+
// If not checked, check it.
|
|
98
|
+
if (!cb.prop('checked')) {
|
|
99
|
+
selectRow();
|
|
100
|
+
} else {
|
|
101
|
+
// If already checked, uncheck?
|
|
102
|
+
cb.prop('checked', false);
|
|
103
|
+
$inputId.val('');
|
|
104
|
+
$inputKey.val('');
|
|
105
|
+
$inputId.trigger('change');
|
|
106
|
+
}
|
|
85
107
|
});
|
|
86
108
|
|
|
87
|
-
// Hover effect
|
|
88
|
-
row.on('mouseenter', function () {
|
|
89
|
-
row.on('mouseleave', function () {
|
|
109
|
+
// Hover effect (Gray, not Blue, matching Read Node)
|
|
110
|
+
row.on('mouseenter', function () { $(this).css('background', '#f7f7f7'); });
|
|
111
|
+
row.on('mouseleave', function () { $(this).css('background', 'transparent'); });
|
|
90
112
|
|
|
91
113
|
$listContainer.append(row);
|
|
92
114
|
});
|
|
93
115
|
|
|
94
|
-
// Scroll to selection
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
97
|
-
$listContainer.scrollTop(
|
|
116
|
+
// Scroll to selection
|
|
117
|
+
const selectedCb = $listContainer.find('.var-checkbox:checked');
|
|
118
|
+
if (selectedCb.length) {
|
|
119
|
+
$listContainer.scrollTop(selectedCb.closest('.var-row').offset().top - $listContainer.offset().top + $listContainer.scrollTop() - 40);
|
|
98
120
|
}
|
|
99
121
|
};
|
|
100
122
|
|
|
@@ -102,7 +124,7 @@
|
|
|
102
124
|
$searchInput.on('keyup', function () {
|
|
103
125
|
const term = $(this).val().toLowerCase();
|
|
104
126
|
$listContainer.find('.var-row').each(function () {
|
|
105
|
-
const text = $(this).find('div:nth-child(3)').text().toLowerCase(); // Key is 3rd column
|
|
127
|
+
const text = $(this).find('div:nth-child(3)').text().toLowerCase(); // Key is 3rd column
|
|
106
128
|
$(this).toggle(text.indexOf(term) > -1);
|
|
107
129
|
});
|
|
108
130
|
});
|
|
@@ -207,7 +229,7 @@
|
|
|
207
229
|
</div>
|
|
208
230
|
|
|
209
231
|
<div style="display:grid; grid-template-columns: 30px 60px 1fr; background:#eee; font-size:11px; font-weight:bold; padding:4px 0; border-bottom:1px solid #ccc;">
|
|
210
|
-
<div style="text-align:center;"></div>
|
|
232
|
+
<div style="text-align:center;"><i class="fa fa-check-square-o"></i></div>
|
|
211
233
|
<div style="text-align:center;">ID</div>
|
|
212
234
|
<div style="padding-left:10px;">Name</div>
|
|
213
235
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-red-contrib-uos-nats",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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",
|