@bldgblocks/node-red-contrib-control 0.1.33 → 0.1.34

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.
@@ -1,4 +1,5 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
2
3
  function GlobalGetterNode(config) {
3
4
  RED.nodes.createNode(this, config);
4
5
  const node = this;
@@ -15,63 +16,50 @@ module.exports = function(RED) {
15
16
  const retryDelays = [0, 100, 500, 1000, 2000, 4000, 8000, 16000];
16
17
  const maxRetries = retryDelays.length - 1;
17
18
 
18
- // --- Process Wrapper and Send Msg ---
19
- function sendValue(storedObject, msgToReuse) {
19
+ // --- Output Helper ---
20
+ function sendValue(storedObject, msgToReuse, done) {
20
21
  const msg = msgToReuse || {};
21
22
 
22
23
  if (storedObject !== undefined && storedObject !== null) {
23
-
24
- // CHECK: Is this our Wrapper Format? (Created by Global Setter)
24
+ // Check if this is our custom wrapper object
25
25
  if (storedObject && typeof storedObject === 'object' && storedObject.hasOwnProperty('value')) {
26
-
27
- // Merge all attributes onto the msg root
28
- // This automatically handles priority, units, metadata, and any future fields
29
26
  if (node.detail === "getObject") {
30
27
  Object.assign(msg, storedObject);
31
28
  }
32
-
33
- // Set the Main Output (e.g. msg.payload = 75)
34
29
  RED.util.setMessageProperty(msg, node.outputProperty, storedObject.value);
35
-
36
30
  } else {
37
- // Handle Legacy/Raw values (not created by Setter)
31
+ // Legacy/Raw values
38
32
  RED.util.setMessageProperty(msg, node.outputProperty, storedObject);
39
33
  msg.metadata = { path: setterNode ? setterNode.varName : "unknown", legacy: true };
40
34
  }
41
35
 
42
- // Update Status
43
36
  let valDisplay = RED.util.getMessageProperty(msg, node.outputProperty);
44
37
  valDisplay = typeof valDisplay === "number" ? valDisplay : valDisplay;
45
- node.status({ fill: "blue", shape: "dot", text: `get: ${valDisplay}` });
46
38
 
47
- node.send(msg);
48
-
39
+ utils.sendSuccess(node, msg, done, `get: ${valDisplay}`, null, "dot");
49
40
  } else {
50
- node.status({ fill: "red", shape: "ring", text: "global variable undefined" });
41
+ utils.sendError(node, msg, done, "global variable undefined");
51
42
  }
52
43
  }
53
44
 
54
- // --- Manage Event Subscription ---
45
+ // --- Connection Logic ---
55
46
  function establishListener() {
56
- // Look for source node
57
47
  setterNode = RED.nodes.getNode(node.targetNodeId);
58
48
 
59
- // If found, subscribe
60
49
  if (setterNode && setterNode.varName && node.updates === 'always') {
61
50
  if (updateListener) {
62
- // Remove existing listener if we're retrying
63
51
  RED.events.removeListener("bldgblocks-global-update", updateListener);
64
52
  }
65
53
 
66
54
  updateListener = function(evt) {
67
55
  if (evt.key === setterNode.varName && evt.store === setterNode.storeName) {
68
- sendValue(evt.data, {});
56
+ // Event Trigger: Pass null for done, as it's not a node input
57
+ sendValue(evt.data, {}, null);
69
58
  }
70
59
  };
71
60
 
72
61
  RED.events.on("bldgblocks-global-update", updateListener);
73
62
 
74
- // Clear retry interval once successful
75
63
  if (retryAction) {
76
64
  clearInterval(retryAction);
77
65
  retryAction = null;
@@ -83,78 +71,67 @@ module.exports = function(RED) {
83
71
  return false;
84
72
  }
85
73
 
86
- // --- Maintain event subscription ---
87
74
  function startHealthCheck() {
88
- const healthCheckAction = () => {
75
+ const check = () => {
89
76
  const listeners = RED.events.listeners("bldgblocks-global-update");
90
77
  const hasOurListener = listeners.includes(updateListener);
91
-
92
78
  if (!hasOurListener) {
93
79
  node.warn("Event listener lost, reconnecting...");
94
80
  if (establishListener()) {
95
81
  node.status({ fill: "green", shape: "dot", text: "Reconnected" });
96
82
  }
97
83
  }
98
-
99
- // Schedule next health check regardless of outcome
100
- setTimeout(healthCheckAction, 30000);
84
+ setTimeout(check, 30000);
101
85
  };
102
- // Inital start
103
- setTimeout(healthCheckAction, 30000);
86
+ setTimeout(check, 30000);
104
87
  }
105
88
 
106
89
  function subscribeWithRetry() {
107
- // Recursive retry
108
90
  retryAction = () => {
109
91
  if (retryCount >= maxRetries) {
110
- node.error("Failed to connect to setter node after multiple attempts");
111
- node.status({ fill: "red", shape: "ring", text: "Connection failed" });
92
+ utils.sendError(node, null, null, "Connection failed");
112
93
  return;
113
94
  }
114
-
115
95
  if (establishListener()) {
116
96
  retryCount = 0;
117
- return; // Success
97
+ return;
118
98
  }
119
-
120
99
  retryCount++;
121
100
  setTimeout(retryAction, retryDelays[Math.min(retryCount, maxRetries - 1)]);
122
101
  };
123
-
124
102
  setTimeout(retryAction, retryDelays[0]);
125
103
  }
126
104
 
127
- // --- HANDLE MANUAL INPUT ---
128
- node.on('input', function(msg, send, done) {
105
+ // --- INPUT HANDLER ---
106
+ node.on('input', async function(msg, send, done) {
129
107
  send = send || function() { node.send.apply(node, arguments); };
130
108
 
131
- setterNode ??= RED.nodes.getNode(node.targetNodeId);
109
+ try {
110
+ setterNode ??= RED.nodes.getNode(node.targetNodeId);
132
111
 
133
- if (setterNode && setterNode.varName) {
134
- const storedObject = node.context().global.get(setterNode.varName, setterNode.storeName);
135
- sendValue(storedObject, msg);
136
- } else {
137
- node.warn("Source node not found or not configured.");
138
- node.status({ fill: "red", shape: "ring", text: "Source node not found" });
112
+ if (setterNode && setterNode.varName) {
113
+ // Async Get
114
+ const storedObject = await utils.getGlobalState(node, setterNode.varName, setterNode.storeName);
115
+ sendValue(storedObject, msg, done);
116
+ } else {
117
+ node.warn("Source node not found or not configured.");
118
+ utils.sendError(node, msg, done, "Source node not found");
119
+ }
120
+ } catch (err) {
121
+ node.error(err);
122
+ utils.sendError(node, msg, done, `Internal Error: ${err.message}`);
139
123
  }
140
-
141
- if (done) done();
142
124
  });
143
125
 
144
- // --- HANDLE REACTIVE UPDATES ---
126
+ // --- INIT ---
145
127
  if (node.updates === 'always') {
146
128
  subscribeWithRetry();
147
129
  startHealthCheck();
148
130
  }
149
131
 
150
- // --- CLEANUP ---
151
132
  node.on('close', function(removed, done) {
152
- if (healthCheckAction) {
153
- clearInterval(healthCheckAction);
154
- }
155
- if (retryAction) {
156
- clearInterval(retryAction);
157
- }
133
+ if (healthCheckAction) clearInterval(healthCheckAction);
134
+ if (retryAction) clearInterval(retryAction);
158
135
  if (removed && updateListener) {
159
136
  RED.events.removeListener("bldgblocks-global-update", updateListener);
160
137
  }
@@ -1,7 +1,5 @@
1
-
2
1
  module.exports = function(RED) {
3
2
  const utils = require('./utils')(RED);
4
-
5
3
  function GlobalSetterNode(config) {
6
4
  RED.nodes.createNode(this, config);
7
5
  const node = this;
@@ -13,238 +11,192 @@ module.exports = function(RED) {
13
11
  node.defaultValue = config.defaultValue;
14
12
  node.writePriority = config.writePriority;
15
13
  node.type = config.defaultValueType;
14
+ node.isBusy = false;
16
15
 
17
- // Cast default value logic
18
16
  if(!isNaN(node.defaultValue) && node.defaultValue !== "") node.defaultValue = Number(node.defaultValue);
19
17
  if(node.defaultValue === "true") node.defaultValue = true;
20
18
  if(node.defaultValue === "false") node.defaultValue = false;
21
19
 
22
- // --- HELPER: Calculate Winner ---
23
- function calculateWinner(state) {
24
- for (let i = 1; i <= 16; i++) {
25
- if (state.priority[i] !== undefined && state.priority[i] !== null) {
26
- return { value: state.priority[i], priority: `${i}` };
20
+ // Helper to generate the data structure
21
+ function buildDefaultState() {
22
+ return {
23
+ payload: node.defaultValue,
24
+ value: node.defaultValue,
25
+ defaultValue: node.defaultValue,
26
+ activePriority: "default",
27
+ units: null,
28
+ priority: { 1: null, 2: null, 3: null, 4: null, 5: null, 6: null, 7: null, 8: null, 9: null, 10: null, 11: null, 12: null, 13: null, 14: null, 15: null, 16: null },
29
+ metadata: {
30
+ sourceId: node.id,
31
+ lastSet: new Date().toISOString(),
32
+ name: node.name || config.path,
33
+ path: node.varName,
34
+ store: node.storeName || 'default',
35
+ type: typeof(node.defaultValue)
27
36
  }
28
- }
29
- return { value: state.defaultValue, priority: "default" };
37
+ };
30
38
  }
31
39
 
32
- function getState() {
40
+ // --- ASYNC INITIALIZATION (IIFE) ---
41
+ // This runs in background immediately after deployment
42
+ (async function initialize() {
33
43
  if (!node.varName) {
34
44
  node.status({ fill: "red", shape: "ring", text: "no variable defined" });
35
- return null;
45
+ return;
36
46
  }
37
-
38
- let state = node.context().global.get(node.varName, node.storeName);
39
- if (!state || typeof state !== 'object' || !state.priority) {
40
- state = {
41
- payload: node.defaultValue,
42
- value: node.defaultValue,
43
- defaultValue: node.defaultValue,
44
- activePriority: "default",
45
- units: null,
46
- priority: {
47
- 1: null,
48
- 2: null,
49
- 3: null,
50
- 4: null,
51
- 5: null,
52
- 6: null,
53
- 7: null,
54
- 8: null,
55
- 9: null,
56
- 10: null,
57
- 11: null,
58
- 12: null,
59
- 13: null,
60
- 14: null,
61
- 15: null,
62
- 16: null,
63
- },
64
- metadata: {
65
- sourceId: node.id,
66
- lastSet: new Date().toISOString(),
67
- name: node.name || config.path,
68
- path: node.varName,
69
- store: node.storeName || 'default',
70
- type: typeof(node.defaultValue)
71
- }
72
- };
73
- return { state: state, existing: false };
47
+ try {
48
+ // Check if data exists
49
+ let state = await utils.getGlobalState(node, node.varName, node.storeName);
50
+ if (!state || typeof state !== 'object' || !state.priority) {
51
+ // If not, set default
52
+ const newState = buildDefaultState();
53
+ await utils.setGlobalState(node, node.varName, node.storeName, newState);
54
+ node.status({ fill: "grey", shape: "dot", text: `initialized: default:${node.defaultValue}` });
55
+ }
56
+ } catch (err) {
57
+ // Silently fail or log if init fails (DB down on boot?)
58
+ node.error(`Init Error: ${err.message}`);
59
+ node.status({ fill: "red", shape: "dot", text: "Init Error" });
74
60
  }
75
- return { state: state, existing: true };
76
- }
77
-
78
- node.isBusy = false;
79
-
80
- const st = getState();
81
- if (st !== null && st.existing === false) {
82
- // Initialize global variable
83
- node.context().global.set(node.varName, st.state, node.storeName);
84
- node.status({ fill: "grey", shape: "dot", text: `initialized: default:${node.defaultValue}` });
85
- }
61
+ })();
86
62
 
63
+ // --- INPUT HANDLER ---
87
64
  node.on('input', async function(msg, send, done) {
88
65
  send = send || function() { node.send.apply(node, arguments); };
89
66
  let prefix = '';
90
- let valPretty = '';
91
-
92
- // Guard against invalid msg
93
- if (!msg) {
94
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
95
- if (done) done();
96
- return;
97
- }
98
67
 
99
- // Evaluate dynamic properties
100
68
  try {
101
-
102
- // Check busy lock
69
+ // Basic Validation
70
+ if (!msg) return utils.sendError(node, msg, done, "invalid message");
71
+
103
72
  if (node.isBusy) {
104
- // Update status to let user know they are pushing too fast
105
73
  node.status({ fill: "yellow", shape: "ring", text: "busy - dropped msg" });
106
74
  if (done) done();
107
75
  return;
108
76
  }
109
-
110
- // Lock node during evaluation
111
77
  node.isBusy = true;
112
78
 
113
- // Begin evaluations
114
- const evaluations = [];
115
-
116
- evaluations.push(
117
- utils.requiresEvaluation(config.writePriorityType)
118
- ? utils.evaluateNodeProperty( config.writePriority, config.writePriorityType, node, msg )
119
- : Promise.resolve(node.writePriority),
120
- );
121
-
122
- const results = await Promise.all(evaluations);
123
-
124
- // Update runtime with evaluated values
125
- node.writePriority = results[0];
126
- } catch (err) {
127
- node.error(`Error evaluating properties: ${err.message}`);
128
- if (done) done();
129
- return;
130
- } finally {
131
- // Release, all synchronous from here on
132
- node.isBusy = false;
133
- }
79
+ // Evaluate Dynamic Properties (Exact same logic as before)
80
+ try {
81
+ const evaluations = [];
82
+ evaluations.push(
83
+ utils.requiresEvaluation(config.writePriorityType)
84
+ ? utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg)
85
+ : Promise.resolve(node.writePriority)
86
+ );
87
+ const results = await Promise.all(evaluations);
88
+ node.writePriority = results[0];
89
+ } catch (err) {
90
+ throw new Error(`Property Eval Error: ${err.message}`);
91
+ } finally {
92
+ node.isBusy = false;
93
+ }
134
94
 
135
- // Get existing state or initialize new
136
- let state = {};
137
- state = getState().state;
95
+ // Get State (Async)
96
+ let state = await utils.getGlobalState(node, node.varName, node.storeName);
97
+ if (!state || typeof state !== 'object' || !state.priority) {
98
+ // Fallback if data is missing (e.g., if message arrives before init finishes)
99
+ state = buildDefaultState();
100
+ }
138
101
 
139
- if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
102
+ // Handle Reload
140
103
  if (msg.context === "reload") {
141
- // Fire Event
142
- RED.events.emit("bldgblocks-global-update", {
143
- key: node.varName,
144
- store: node.storeName,
145
- data: state
146
- });
104
+ RED.events.emit("bldgblocks-global-update", { key: node.varName, store: node.storeName, data: state });
105
+ await utils.setGlobalState(node, node.varName, node.storeName, state);
147
106
 
148
- // Send flow
149
- node.context().global.set(node.varName, state, node.storeName);
150
107
  prefix = state.activePriority === 'default' ? '' : 'P';
151
- node.status({ fill: "green", shape: "dot", text: `reload: ${prefix}${state.activePriority}:${state.value}${state.units}` });
152
- node.send({ ...state });
153
- if (done) done();
154
- return;
108
+ const statusText = `reload: ${prefix}${state.activePriority}:${state.value}${state.units}`;
109
+
110
+ return utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
155
111
  }
156
- }
157
112
 
158
- const inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
159
- try {
113
+ // Get Input Value
114
+ const inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
160
115
  if (inputValue === undefined) {
161
- node.status({ fill: "red", shape: "ring", text: `msg.${node.inputProperty} not found` });
162
- if (done) done();
163
- return;
116
+ return utils.sendError(node, msg, done, `msg.${node.inputProperty} not found`);
117
+ }
118
+
119
+ // Update State
120
+ if (node.writePriority === 'default') {
121
+ state.defaultValue = inputValue === null || inputValue === "null" ? node.defaultValue : inputValue;
122
+ } else {
123
+ const priority = parseInt(node.writePriority, 10);
124
+ if (isNaN(priority) || priority < 1 || priority > 16) {
125
+ return utils.sendError(node, msg, done, `Invalid priority: ${node.writePriority}`);
126
+ }
127
+ if (inputValue !== undefined) {
128
+ state.priority[node.writePriority] = inputValue;
129
+ }
130
+ }
131
+
132
+ if (state.defaultValue === null || state.defaultValue === "null" || state.defaultValue === undefined) {
133
+ state.defaultValue = node.defaultValue;
164
134
  }
165
- } catch (error) {
166
- node.status({ fill: "red", shape: "ring", text: `Error accessing msg.${node.inputProperty}` });
167
- if (done) done();
168
- return;
169
- }
170
135
 
136
+ // Calculate Winner
137
+ const { value, priority } = utils.getHighestPriority(state);
171
138
 
172
- // Update Default, can not be set null
173
- if (node.writePriority === 'default') {
174
- state.defaultValue = inputValue === null || inputValue === "null" ? node.defaultValue : inputValue;
175
- } else {
176
- const priority = parseInt(node.writePriority, 10);
177
- if (isNaN(priority) || priority < 1 || priority > 16) {
178
- node.status({ fill: "red", shape: "ring", text: `Invalid priority: ${node.writePriority}` });
139
+ // Check for change
140
+ if (value === state.value && priority === state.activePriority) {
141
+ prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
142
+ const noChangeText = `no change: ${prefix}${node.writePriority}:${state.value}${state.units}`;
143
+ node.status({ fill: "green", shape: "dot", text: noChangeText });
179
144
  if (done) done();
180
145
  return;
181
146
  }
182
-
183
- if (inputValue !== undefined) {
184
- state.priority[node.writePriority] = inputValue;
147
+
148
+ // Update Values
149
+ state.payload = value;
150
+ state.value = value;
151
+ state.activePriority = priority;
152
+
153
+ state.metadata.sourceId = node.id;
154
+ state.metadata.lastSet = new Date().toISOString();
155
+ state.metadata.name = node.name || config.path;
156
+ state.metadata.path = node.varName;
157
+ state.metadata.store = node.storeName || 'default';
158
+ state.metadata.type = typeof(value) || node.type;
159
+
160
+ // Capture Units
161
+ let capturedUnits = null;
162
+ if (msg.units !== undefined) {
163
+ capturedUnits = msg.units;
164
+ } else if (inputValue !== null && typeof inputValue === 'object' && inputValue.units) {
165
+ capturedUnits = inputValue.units;
185
166
  }
186
- }
187
-
188
- // Ensure defaultValue always has a value
189
- if (state.defaultValue === null || state.defaultValue === "null" || state.defaultValue === undefined) {
190
- state.defaultValue = node.defaultValue;
191
- }
167
+ state.units = capturedUnits;
168
+
169
+ // Save (Async) and Emit
170
+ await utils.setGlobalState(node, node.varName, node.storeName, state);
192
171
 
193
- // Calculate Winner
194
- const { value, priority } = calculateWinner(state);
195
- if (value === state.value && priority === state.activePriority) {
196
- // No change, exit early
197
172
  prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
198
- node.status({ fill: "green", shape: "dot", text: `no change: ${prefix}${node.writePriority}:${state.value}${state.units}` });
199
- if (done) done();
200
- return;
201
- }
202
- state.payload = value;
203
- state.value = value;
204
- state.activePriority = priority;
205
-
206
- // Update Metadata
207
- state.metadata.sourceId = node.id;
208
- state.metadata.lastSet = new Date().toISOString();
209
- state.metadata.name = node.name || config.path;
210
- state.metadata.path = node.varName;
211
- state.metadata.store = node.storeName || 'default';
212
- state.metadata.type = typeof(value) || node.type;
213
-
214
- // Units logic
215
- let capturedUnits = null;
216
- if (msg.units !== undefined) {
217
- capturedUnits = msg.units;
218
- } else if (inputValue !== null && typeof inputValue === 'object' && inputValue.units) {
219
- capturedUnits = inputValue.units;
220
- }
173
+ const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
174
+ const statusText = `write: ${prefix}${node.writePriority}:${inputValue}${state.units} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units}`;
175
+
176
+ RED.events.emit("bldgblocks-global-update", {
177
+ key: node.varName,
178
+ store: node.storeName,
179
+ data: state
180
+ });
181
+
182
+ utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
221
183
 
222
- state.units = capturedUnits;
223
-
224
- // Save & Emit
225
- node.context().global.set(node.varName, state, node.storeName);
226
- prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
227
- const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
228
- node.status({ fill: "blue", shape: "dot", text: `write: ${prefix}${node.writePriority}:${inputValue}${state.units} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units}` });
229
-
230
- // Fire Event
231
- RED.events.emit("bldgblocks-global-update", {
232
- key: node.varName,
233
- store: node.storeName,
234
- data: state
235
- });
236
-
237
- // Send copy
238
- node.send({ ...state });
239
- if (done) done();
184
+ } catch (err) {
185
+ node.error(err);
186
+ utils.sendError(node, msg, done, `Internal Error: ${err.message}`);
187
+ }
240
188
  });
241
189
 
242
190
  node.on('close', function(removed, done) {
243
191
  if (removed && node.varName) {
244
192
  RED.events.removeAllListeners("bldgblocks-global-update");
245
- node.context().global.set(node.varName, undefined, node.storeName);
193
+ // Callback style safe for close
194
+ node.context().global.set(node.varName, undefined, node.storeName, function() {
195
+ done();
196
+ });
197
+ } else {
198
+ done();
246
199
  }
247
- done();
248
200
  });
249
201
  }
250
202
  RED.nodes.registerType("global-setter", GlobalSetterNode);
@@ -45,8 +45,8 @@
45
45
  Reads a network point by pointId.
46
46
 
47
47
  ### Input
48
- : payload (object) : A command object containing
49
- * `pointId` (number): The integer ID of the point.
48
+ : action (string) : Not used by the node, used for routing to the correct node over the network when received.
49
+ : pointId (number) : The integer ID of the point.
50
50
 
51
51
  ### Output
52
52
  : payload (object) : Global data object
@@ -1,53 +1,51 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
2
3
  function NetworkReadNode(config) {
3
4
  RED.nodes.createNode(this, config);
4
5
  const node = this;
5
-
6
6
  node.registry = RED.nodes.getNode(config.registry);
7
7
 
8
- node.on("input", function(msg, send, done) {
8
+ node.on("input", async function(msg, send, done) {
9
9
  send = send || function() { node.send.apply(node, arguments); };
10
10
 
11
- let currentPath = null;
12
- let currentStore = "default";
13
-
14
- if (node.registry) {
15
- let currentEntry = node.registry.lookup(msg.pointId);
16
-
17
- if (!currentEntry) {
18
- node.status({ fill: "red", shape: "ring", text: `Requested pointId not registered` });
19
- msg.status = { status: "fail", pointId: msg.pointId, error: `Point Not Registered: ${msg.pointId}` };
20
- node.send(msg);
11
+ try {
12
+ if (!node.registry) {
13
+ node.status({ fill: "red", shape: "ring", text: "Registry missing" });
21
14
  if (done) done();
22
15
  return;
23
16
  }
17
+
18
+ const currentEntry = node.registry.lookup(msg.pointId);
19
+ if (!currentEntry) {
20
+ return utils.sendError(node, msg, done, `Not Registered: ${msg.pointId}`, msg.pointId);
21
+ }
24
22
 
25
- currentPath = currentEntry.path;
26
- currentStore = currentEntry.store || "default";
27
- let globalData = node.context().global.get(currentPath, currentStore) || {};
28
-
29
- if (globalData === null || Object.keys(globalData).length === 0) {
30
- node.status({ fill: "red", shape: "ring", text: `Global data doesn't exist, waiting...` });
31
- msg.status = { status: "fail", pointId: msg.pointId, error: `Point Not Found: ${msg.pointId}` };
32
- node.send(msg);
33
- if (done) done();
34
- return;
23
+ const currentPath = currentEntry.path;
24
+ const currentStore = currentEntry.store || "default";
25
+
26
+ // Async Get
27
+ let globalData = await utils.getGlobalState(node, currentPath, currentStore);
28
+
29
+ if (!globalData || Object.keys(globalData).length === 0) {
30
+ return utils.sendError(node, msg, done, `Global Data Empty: ${msg.pointId}`, msg.pointId);
35
31
  }
36
32
 
37
33
  msg = { ...globalData };
38
- node.status({ fill: "blue", shape: "ring", text: `Read (${currentStore})::${msg.metadata.name}::${msg.network.pointId} ` });
39
- msg.status = { status: "ok", pointId: msg.network.pointId, message: `Data Found. pointId: ${msg.network.pointId} value: ${msg.value}` };
40
- node.send(msg);
41
34
 
42
- if (done) done();
43
- } else {
44
- node.status({ fill: "red", shape: "ring", text: `Registry not found. Create config node.` });
45
- if (done) done();
46
- return;
35
+ const ptName = msg.metadata?.name ?? "Unknown";
36
+ const ptVal = msg.value !== undefined ? msg.value : "No Value";
37
+ const ptId = msg.network?.pointId ?? msg.pointId;
38
+
39
+ const msgText = `Data Found. pointId: ${ptId} value: ${ptVal}`;
40
+
41
+ utils.sendSuccess(node, msg, done, msgText, ptId, "ring");
42
+
43
+ } catch (err) {
44
+ node.error(err);
45
+ utils.sendError(node, msg, done, `Internal Error: ${err.message}`, msg?.pointId);
47
46
  }
48
47
  });
49
48
 
50
- // Cleanup
51
49
  node.on('close', function(removed, done) {
52
50
  if (removed && node.registry) {
53
51
  node.registry.unregister(node.pointId, node.id);
@@ -1,9 +1,9 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
2
3
  function NetworkRegisterNode(config) {
3
4
  RED.nodes.createNode(this, config);
4
5
  const node = this;
5
6
 
6
- // Config
7
7
  node.registry = RED.nodes.getNode(config.registry);
8
8
  node.pointId = parseInt(config.pointId);
9
9
  node.writable = !!config.writable;
@@ -12,7 +12,7 @@ module.exports = function(RED) {
12
12
  // Initial Registration
13
13
  if (node.registry && !isNaN(node.pointId)) {
14
14
  const success = node.registry.register(node.pointId, {
15
- nodeId: node.id, // for point registry collision checks
15
+ nodeId: node.id,
16
16
  writable: node.writable,
17
17
  path: "not ready",
18
18
  store: "not ready"
@@ -20,136 +20,87 @@ module.exports = function(RED) {
20
20
 
21
21
  if (success) {
22
22
  node.isRegistered = true;
23
- node.status({ fill: "blue", shape: "ring", text: `ID: ${node.pointId} (Waiting)` });
23
+ node.status({ fill: "yellow", shape: "ring", text: `ID: ${node.pointId} (Waiting)` });
24
24
  } else {
25
25
  node.error(`Point ID ${node.pointId} is already in use.`);
26
26
  node.status({ fill: "red", shape: "dot", text: "ID Conflict" });
27
27
  }
28
28
  }
29
29
 
30
- node.on("input", function(msg, send, done) {
30
+ node.on("input", async function(msg, send, done) {
31
31
  send = send || function() { node.send.apply(node, arguments); };
32
32
 
33
- // Nothing to do. Return.
34
- if (!node.isRegistered) {
35
- node.status({ fill: "red", shape: "ring", text: `Not registered` });
36
- if (done) done();
37
- return;
38
- }
39
-
40
- if (!msg || typeof msg !== "object") {
41
- const message = `Invalid msg.`;
42
- node.status({ fill: "red", shape: "ring", text: `${message}` });
43
- if (done) done();
44
- return;
45
- }
46
-
47
- if (!node.registry) {
48
- const message = `Registry not found. Create config node.`;
49
- node.status({ fill: "red", shape: "ring", text: `${message}` });
50
- msg.status = { status: "fail", pointId: node.pointId, error: `${message}` };
51
- node.send(msg);
52
- if (done) done();
53
- return;
54
- }
55
-
56
- // Message should contain data & metadata from a global setter node
57
- const missingFields = [];
58
-
59
- if (!msg.metadata) missingFields.push("metadata");
60
- if (msg.value === undefined) missingFields.push("value");
61
- if (msg.units === undefined) missingFields.push("units");
62
- if (!msg.activePriority) missingFields.push("activePriority");
63
-
64
- // Check nested metadata properties
65
- if (msg.metadata) {
66
- if (!msg.metadata.path) missingFields.push("metadata.path");
67
- if (!msg.metadata.store) missingFields.push("metadata.store");
68
- if (!msg.metadata.sourceId) missingFields.push("metadata.sourceId");
69
- } else {
70
- missingFields.push("metadata (entire object)");
71
- }
72
-
73
- if (missingFields.length > 0) {
74
- const specificMessage = `Missing required fields: ${missingFields.join(', ')}`;
75
- node.status({
76
- fill: "red",
77
- shape: "ring",
78
- text: `${missingFields.length} missing: ${missingFields.slice(0, 3).join(', ')}${missingFields.length > 3 ? '...' : ''}`
79
- });
80
-
81
- node.send(msg);
82
- if (done) done();
83
- return;
84
- }
33
+ try {
34
+ // Pre-flight
35
+ if (!node.isRegistered) return utils.sendError(node, null, done, "Node not registered");
36
+ if (!msg || typeof msg !== "object") return utils.sendError(node, null, done, "Invalid msg object");
37
+ if (!node.registry) return utils.sendError(node, msg, done, "Registry config missing", node.pointId);
85
38
 
39
+ // Validate Fields
40
+ if (!msg.activePriority || !msg.metadata?.path || !msg.metadata?.store) {
41
+ return utils.sendError(node, msg, done, "Missing required fields (metadata.path/store, activePriority)", node.pointId);
42
+ }
86
43
 
87
- // Lookup current registration
88
- let pointData = node.registry.lookup(node.pointId);
44
+ // Logic & State Update
45
+ let pointData = node.registry.lookup(node.pointId);
89
46
 
90
- const incoming = {
91
- writable: node.writable,
92
- path: msg.metadata.path,
93
- store: msg.metadata.store
94
- };
95
-
96
- // Update Registry on change
97
- if (!pointData
98
- || pointData.nodeId !== node.nodeId
99
- || pointData.writable !== incoming.writable
100
- || pointData.path !== incoming.path
101
- || pointData.store !== incoming.store) {
102
-
103
- node.registry.register(node.pointId, {
104
- nodeId: node.id, // for point registry collision checks
47
+ const incoming = {
105
48
  writable: node.writable,
106
49
  path: msg.metadata.path,
107
50
  store: msg.metadata.store
108
- });
109
-
110
- pointData = node.registry.lookup(node.pointId);
111
-
112
- let globalData = {};
113
- globalData = node.context().global.get(pointData.path, pointData.store);
114
-
115
- if (globalData === null || Object.keys(globalData).length === 0) {
116
- const message = `Global data doesn't exist for (${pointData.store ?? "default"})::${pointData.path}::${node.pointId}`;
117
- node.status({ fill: "red", shape: "ring", text: `${message}` });
118
- msg.status = { status: "fail", pointId: node.pointId, error: `${message}` };
119
- if (done) done();
120
- return;
121
- }
51
+ };
52
+
53
+ const needsUpdate = !pointData
54
+ || pointData.nodeId !== node.id
55
+ || pointData.writable !== incoming.writable
56
+ || pointData.path !== incoming.path
57
+ || pointData.store !== incoming.store;
58
+
59
+ if (needsUpdate) {
60
+ node.registry.register(node.pointId, {
61
+ nodeId: node.id,
62
+ writable: node.writable,
63
+ path: incoming.path,
64
+ store: incoming.store
65
+ });
66
+
67
+ pointData = node.registry.lookup(node.pointId);
68
+ const currentStore = pointData.store || "default";
69
+
70
+ // Async Get
71
+ const globalData = await utils.getGlobalState(node, pointData.path, currentStore);
72
+
73
+ if (!globalData || Object.keys(globalData).length === 0) {
74
+ return utils.sendError(node, msg, done, `Global missing: (${currentStore})::${pointData.path}`, node.pointId);
75
+ }
76
+
77
+ const networkObject = {
78
+ ...globalData,
79
+ network: {
80
+ registry: node.registry.name,
81
+ pointId: node.pointId,
82
+ writable: node.writable
83
+ }
84
+ };
85
+
86
+ // Async Set
87
+ await utils.setGlobalState(node, pointData.path, currentStore, networkObject);
122
88
 
123
- let network = {
124
- registry: node.registry.name,
125
- pointId: node.pointId,
126
- writable: node.writable
89
+ const statusText = `Registered: (${currentStore})::${pointData.path}::${node.pointId}`;
90
+ return utils.sendSuccess(node, networkObject, done, statusText, node.pointId, "dot");
127
91
  }
128
92
 
129
- const networkObject = { ...globalData, network: network};
130
- const message = `Registered: (${pointData.store ?? "default"})::${pointData.path}::${node.pointId}`;
131
-
132
- node.context().global.set(pointData.path, networkObject, pointData.store);
133
- node.status({ fill: "blue", shape: "dot", text: `${message}` });
134
- msg.status = { status: "success", pointId: node.pointId, error: `${message}` };
93
+ // Passthrough
94
+ const prefix = msg.activePriority === 'default' ? '' : 'P';
95
+ const statusText = `Passthrough: ${prefix}${msg.activePriority}:${msg.value}${msg.units}`;
96
+ utils.sendSuccess(node, msg, done, statusText, node.pointId, "ring");
135
97
 
136
- node.send(networkObject);
137
- if (done) done();
138
- return;
98
+ } catch (err) {
99
+ node.error(err);
100
+ utils.sendError(node, msg, done, `Internal Error: ${err.message}`, node.pointId);
139
101
  }
140
-
141
- // Make it here, then message should match global and ready to go
142
- // Pass through msg
143
- const prefix = msg.activePriority === 'default' ? '' : 'P';
144
- const message = `Passthrough: ${prefix}${msg.activePriority}:${msg.value}${msg.units}`;
145
- node.status({ fill: "blue", shape: "ring", text: message });
146
-
147
- node.send(msg);
148
- if (done) done();
149
- return;
150
102
  });
151
103
 
152
- // Cleanup
153
104
  node.on('close', function(removed, done) {
154
105
  if (removed && node.registry && node.isRegistered) {
155
106
  node.registry.unregister(node.pointId, node.pointId);
@@ -47,6 +47,7 @@ Writes network commands to Global Variables using the Priority Array logic.
47
47
 
48
48
  ### Input
49
49
  : payload (object) : A command object containing
50
+ * `action` (string): Not used by the node, used for routing to the correct node over the network when received.
50
51
  * `pointId` (number): The integer ID of the point.
51
52
  * `priority` (number): The priority level (1-16) to write to.
52
53
  * `value` (any): The value to set. Send `null` to relinquish (clear) this priority level.
@@ -1,125 +1,82 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
2
3
  function NetworkWriteNode(config) {
3
4
  RED.nodes.createNode(this, config);
4
5
  const node = this;
5
-
6
6
  node.registry = RED.nodes.getNode(config.registry);
7
7
 
8
- node.on("input", function(msg, send, done) {
8
+ node.on("input", async function(msg, send, done) {
9
9
  send = send || function() { node.send.apply(node, arguments); };
10
10
 
11
- // Expecting: msg.payload = { pointId, priority, value }
12
- if (!msg || !msg.pointId || !msg.priority || msg.value === undefined) {
13
- node.status({ fill: "red", shape: "dot", text: "Invalid msg properties" });
14
- msg.status = { status: "fail", pointId: msg.pointId, error: `Invalid msg properties` };
15
-
16
- node.send(msg);
17
- if (done) done();
18
- return;
19
- }
20
-
21
- // Lookup Path
22
- const entry = node.registry.lookup(msg.pointId);
23
- const store = entry.store ?? "default";
24
- const path = entry.path;
25
- if (!entry || !path) {
26
- node.status({ fill: "red", shape: "dot", text: `Unknown ID: (${store})::${path}::${msg.pointId}` });
27
- msg.status = { status: "fail", pointId: msg.pointId, error: `Unknown ID: (${store})::${path}::${msg.pointId}` };
11
+ try {
12
+ // Validation
13
+ if (!msg || !msg.pointId || !msg.priority || msg.value === undefined) {
14
+ return utils.sendError(node, msg, done, "Invalid msg properties", msg?.pointId);
15
+ }
28
16
 
29
- node.send(msg);
30
- if (done) done();
31
- return;
32
- }
33
-
34
- // Check Writable
35
- if (!entry.writable) {
36
- node.status({ fill: "red", shape: "dot", text: `Not Writable: (${store})::${path}::${msg.pointId}` });
37
- msg.status = { status: "fail", pointId: msg.pointId, error: `Not Writable: (${store})::${path}::${msg.pointId}` };
17
+ // Registry Lookup
18
+ const entry = node.registry.lookup(msg.pointId);
19
+ if (!entry?.path) {
20
+ const store = entry?.store ?? "unknown";
21
+ return utils.sendError(node, msg, done, `Unknown ID: (${store})::${msg.pointId}`, msg.pointId);
22
+ }
38
23
 
39
- node.send(msg);
40
- if (done) done();
41
- return;
42
- }
24
+ const { store = "default", path, writable } = entry;
43
25
 
44
- // Get State
45
- const globalContext = node.context().global;
46
- let state = globalContext.get(path, store);
26
+ if (!writable) {
27
+ return utils.sendError(node, msg, done, `Not Writable: (${store})::${path}::${msg.pointId}`, msg.pointId);
28
+ }
47
29
 
48
- if (!state || !state.priority) {
49
- node.status({ fill: "red", shape: "ring", text: `Point Not Found: (${store})::${path}::${msg.pointId}` });
50
- msg.status = { status: "fail", pointId: msg.pointId, error: `Point Not Found: (${store})::${path}::${msg.pointId}` };
30
+ // Get State (Async)
31
+ let state = await utils.getGlobalState(node, path, store);
51
32
 
52
- node.send(msg);
53
- if (done) done();
54
- return;
55
- }
33
+ if (!state || !state.priority) {
34
+ return utils.sendError(node, msg, done, `Point Not Found: (${store})::${path}`, msg.pointId);
35
+ }
56
36
 
57
- // Check Type
58
- if (msg.value === "null" || msg.value === null) {
59
- msg.value = null;
60
- } else {
61
- const inputType = typeof msg.value;
62
- const dataType = state.metadata.type;
63
- if (inputType !== dataType) {
64
- node.status({ fill: "red", shape: "ring", text: `Mismatch type error: ${store}:${path} ID: ${msg.pointId}, ${inputType} !== ${dataType}` });
65
- msg.status = { status: "fail", pointId: msg.pointId, error: `Mismatch type error: ${store}:${path} ID: ${msg.pointId}, ${inputType} !== ${dataType}` };
37
+ // Type Check
38
+ let newValue = msg.value === "null" || msg.value === null ? null : msg.value;
39
+ if (newValue !== null) {
40
+ const dataType = state.metadata?.type;
41
+ if (dataType && typeof newValue !== dataType) {
42
+ return utils.sendError(node, msg, done, `Type Mismatch: Expected ${dataType}`, msg.pointId);
43
+ }
44
+ }
66
45
 
67
- node.send(msg);
68
- if (done) done();
69
- return;
46
+ // Update Priority Logic
47
+ if (msg.priority === 'default') {
48
+ state.defaultValue = newValue ?? state.defaultValue;
49
+ } else {
50
+ const priority = parseInt(msg.priority, 10);
51
+ if (isNaN(priority) || priority < 1 || priority > 16) {
52
+ return utils.sendError(node, msg, done, `Invalid Priority: ${msg.priority}`, msg.pointId);
53
+ }
54
+ state.priority[msg.priority] = newValue;
70
55
  }
71
- }
72
56
 
73
- // Update Priority
74
- if (msg.priority === 'default') {
75
- state.defaultValue = msg.value ?? state.defaultValue;
76
- } else {
77
- const priority = parseInt(msg.priority, 10);
78
- if (isNaN(priority) || priority < 1 || priority > 16) {
79
- node.status({ fill: "red", shape: "ring", text: `Invalid priority: ${msg.priority}` });
80
- msg.status = { status: "fail", pointId: msg.pointId, error: `Invalid Priority: (${store})::${path}::${msg.pointId}` };
57
+ // Calculate Winner
58
+ const result = utils.getHighestPriority(state);
59
+ state.value = result.value;
60
+ state.activePriority = result.priority;
61
+ state.metadata.lastSet = new Date().toISOString();
81
62
 
82
- node.send(msg);
83
- if (done) done();
84
- return;
85
- }
86
-
87
- state.priority[msg.priority] = msg.value;
88
- }
63
+ // Save (Async) & Emit
64
+ await utils.setGlobalState(node, path, store, state);
89
65
 
90
- // Calculate Winner (Same logic as Setter)
91
- let winnerValue = state.defaultValue;
92
- let winnerPriority = 'default'
93
- for (let i = 1; i <= 16; i++) {
94
- if (state.priority[i] !== undefined && state.priority[i] !== null) {
95
- winnerValue = state.priority[i];
96
- winnerPriority = `${i}`
97
- break;
98
- }
99
- }
100
- state.value = winnerValue;
101
- state.activePriority = winnerPriority;
102
- state.metadata.lastSet = new Date().toISOString();
66
+ const prefixReq = msg.priority === 'default' ? '' : 'P';
67
+ const prefixAct = state.activePriority === 'default' ? '' : 'P';
68
+ const statusMsg = `Wrote: ${prefixReq}${msg.priority}:${newValue} > Active: ${prefixAct}${state.activePriority}:${state.value}`;
103
69
 
104
- // Save & Emit
105
- const prefix1 = msg.priority === 'default' ? '' : 'P';
106
- const prefix2 = state.activePriority === 'default' ? '' : 'P';
107
- const message = `Wrote: ${prefix1}${msg.priority}:${msg.value} > (${store})::${path}::${msg.pointId} Active: ${prefix2}${winnerPriority}:${winnerValue}`;
108
- node.status({ fill: "blue", shape: "ring", text: message });
70
+ msg = { ...state, status: null };
71
+
72
+ RED.events.emit("bldgblocks-global-update", { key: path, store: store, data: state });
109
73
 
110
- globalContext.set(path, state, store);
111
- msg = { ...state };
112
- msg.status = { status: "ok", pointId: msg.pointId, message: message };
113
-
114
- // Trigger global getters to update on new value
115
- RED.events.emit("bldgblocks-global-update", {
116
- key: path,
117
- store: store,
118
- data: state
119
- });
74
+ utils.sendSuccess(node, msg, done, statusMsg, msg.pointId, "ring");
120
75
 
121
- node.send(msg);
122
- if (done) done();
76
+ } catch (err) {
77
+ node.error(err);
78
+ utils.sendError(node, msg, done, `Internal Error: ${err.message}`, msg?.pointId);
79
+ }
123
80
  });
124
81
  }
125
82
  RED.nodes.registerType("network-write", NetworkWriteNode);
package/nodes/utils.js CHANGED
@@ -14,12 +14,81 @@ module.exports = function(RED) {
14
14
  }
15
15
  });
16
16
  }
17
+
18
+ function sendError(node, msg, done, text, pointId = null) {
19
+ node.status({ fill: "red", shape: "dot", text: text });
20
+
21
+ // Only attempt to send if a message object exists
22
+ if (msg) {
23
+ msg.status = {
24
+ code: "error",
25
+ pointId: pointId || msg.pointId || "unknown",
26
+ message: text
27
+ };
28
+ node.send(msg);
29
+ }
30
+
31
+ if (done) done();
32
+ }
33
+
34
+ function sendSuccess(node, msg, done, text, pointId, shape = "ring") {
35
+ node.status({ fill: "blue", shape: shape, text: text });
36
+
37
+ if (msg) {
38
+ msg.status = {
39
+ code: "ok",
40
+ pointId: pointId,
41
+ message: text
42
+ };
43
+ node.send(msg);
44
+ }
45
+
46
+ if (done) done();
47
+ }
48
+
49
+ function getGlobalState(node, path, store) {
50
+ return new Promise((resolve, reject) => {
51
+ node.context().global.get(path, store, (err, data) => {
52
+ if (err) reject(err);
53
+ else resolve(data);
54
+ });
55
+ });
56
+ }
57
+
58
+ function setGlobalState(node, path, store, value) {
59
+ return new Promise((resolve, reject) => {
60
+ node.context().global.set(path, value, store, (err) => {
61
+ if (err) reject(err);
62
+ else resolve();
63
+ });
64
+ });
65
+ }
66
+
67
+ function getHighestPriority(state) {
68
+ let value = state.defaultValue;
69
+ let priority = 'default';
70
+
71
+ for (let i = 1; i <= 16; i++) {
72
+ // Check strictly for undefined/null, allow 0 or false
73
+ if (state.priority[i] !== undefined && state.priority[i] !== null) {
74
+ value = state.priority[i];
75
+ priority = String(i);
76
+ break;
77
+ }
78
+ }
79
+ return { value, priority };
80
+ }
17
81
 
18
82
  // Usage:
19
83
  // const utils = require('./utils')(RED);
20
84
 
21
85
  return {
22
86
  requiresEvaluation,
23
- evaluateNodeProperty
87
+ evaluateNodeProperty,
88
+ sendError,
89
+ sendSuccess,
90
+ getGlobalState,
91
+ setGlobalState,
92
+ getHighestPriority
24
93
  };
25
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.1.33",
3
+ "version": "0.1.34",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [ "node-red", "sedona", "control", "hvac" ],
6
6
  "files": ["nodes/*.js", "nodes/*.html"],