@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.
- package/nodes/global-getter.js +33 -56
- package/nodes/global-setter.js +137 -185
- package/nodes/network-read.html +2 -2
- package/nodes/network-read.js +29 -31
- package/nodes/network-register.js +62 -111
- package/nodes/network-write.html +1 -0
- package/nodes/network-write.js +57 -100
- package/nodes/utils.js +70 -1
- package/package.json +1 -1
package/nodes/global-getter.js
CHANGED
|
@@ -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
|
-
// ---
|
|
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
|
-
//
|
|
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
|
-
|
|
48
|
-
|
|
39
|
+
utils.sendSuccess(node, msg, done, `get: ${valDisplay}`, null, "dot");
|
|
49
40
|
} else {
|
|
50
|
-
|
|
41
|
+
utils.sendError(node, msg, done, "global variable undefined");
|
|
51
42
|
}
|
|
52
43
|
}
|
|
53
44
|
|
|
54
|
-
// ---
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
// ---
|
|
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
|
-
|
|
109
|
+
try {
|
|
110
|
+
setterNode ??= RED.nodes.getNode(node.targetNodeId);
|
|
132
111
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
}
|
package/nodes/global-setter.js
CHANGED
|
@@ -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
|
-
//
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
45
|
+
return;
|
|
36
46
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
state
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
102
|
+
// Handle Reload
|
|
140
103
|
if (msg.context === "reload") {
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
159
|
-
|
|
113
|
+
// Get Input Value
|
|
114
|
+
const inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
|
|
160
115
|
if (inputValue === undefined) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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);
|
package/nodes/network-read.html
CHANGED
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
Reads a network point by pointId.
|
|
46
46
|
|
|
47
47
|
### Input
|
|
48
|
-
:
|
|
49
|
-
|
|
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
|
package/nodes/network-read.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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,
|
|
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: "
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
node.
|
|
36
|
-
if (
|
|
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
|
-
|
|
88
|
-
|
|
44
|
+
// Logic & State Update
|
|
45
|
+
let pointData = node.registry.lookup(node.pointId);
|
|
89
46
|
|
|
90
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
node.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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);
|
package/nodes/network-write.html
CHANGED
|
@@ -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.
|
package/nodes/network-write.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
40
|
-
if (done) done();
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
24
|
+
const { store = "default", path, writable } = entry;
|
|
43
25
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
26
|
+
if (!writable) {
|
|
27
|
+
return utils.sendError(node, msg, done, `Not Writable: (${store})::${path}::${msg.pointId}`, msg.pointId);
|
|
28
|
+
}
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
33
|
+
if (!state || !state.priority) {
|
|
34
|
+
return utils.sendError(node, msg, done, `Point Not Found: (${store})::${path}`, msg.pointId);
|
|
35
|
+
}
|
|
56
36
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
state.
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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