@bldgblocks/node-red-contrib-control 0.2.2 → 0.2.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.
@@ -9,14 +9,26 @@ module.exports = function(RED) {
9
9
  node.name = config.name;
10
10
  node.comment = config.comment || "";
11
11
  node.statusDisplay = config.statusDisplay;
12
+ node.statusProperty = config.statusProperty || "";
13
+ node.statusPropertyType = config.statusPropertyType || "msg";
14
+
15
+ // Pre-compile JSONata expression if configured
16
+ let jsonataExpr = null;
17
+ if (node.statusPropertyType === "jsonata" && node.statusProperty) {
18
+ try {
19
+ jsonataExpr = RED.util.prepareJSONataExpression(node.statusProperty, node);
20
+ } catch (err) {
21
+ node.error(`Invalid JSONata expression: ${err.message}`);
22
+ }
23
+ }
12
24
 
13
25
  // Ensure comment is within 100 characters
14
26
  if (node.comment && node.comment.length > 100) {
15
27
  node.comment = node.comment.substring(0, 100);
16
28
  }
17
29
 
18
- // Update status based on configuration
19
- const updateStatus = function() {
30
+ // Update status based on configuration (static — no msg available)
31
+ const updateStaticStatus = function() {
20
32
  switch (node.statusDisplay) {
21
33
  case "default":
22
34
  utils.setStatusOK(node, node.comment || "No comment set");
@@ -24,16 +36,54 @@ module.exports = function(RED) {
24
36
  case "name":
25
37
  utils.setStatusOK(node, node.name || "comment");
26
38
  break;
39
+ case "property":
40
+ // Can't resolve msg properties without a message — show placeholder
41
+ utils.setStatusOK(node, "waiting for input");
42
+ break;
27
43
  case "none":
28
44
  default:
29
- // No status for "none"
30
45
  break;
31
46
  }
32
47
  };
33
48
 
34
- updateStatus();
49
+ // Resolve and display the configured status property from a message
50
+ function resolveStatusProperty(msg) {
51
+ return new Promise(function(resolve) {
52
+ if (node.statusPropertyType === "jsonata" && jsonataExpr) {
53
+ RED.util.evaluateJSONataExpression(jsonataExpr, msg, function(err, result) {
54
+ if (err) {
55
+ resolve(undefined);
56
+ } else {
57
+ resolve(result);
58
+ }
59
+ });
60
+ } else {
61
+ // msg type — use getMessageProperty
62
+ try {
63
+ resolve(RED.util.getMessageProperty(msg, node.statusProperty));
64
+ } catch (err) {
65
+ resolve(undefined);
66
+ }
67
+ }
68
+ });
69
+ }
70
+
71
+ function formatValue(val) {
72
+ if (val === undefined) return "undefined";
73
+ if (val === null) return "null";
74
+ if (typeof val === "number") return val % 1 === 0 ? String(val) : val.toFixed(2);
75
+ if (typeof val === "boolean") return String(val);
76
+ if (typeof val === "string") return val.length > 40 ? val.substring(0, 40) + "…" : val;
77
+ if (typeof val === "object") {
78
+ const s = JSON.stringify(val);
79
+ return s.length > 40 ? s.substring(0, 40) + "…" : s;
80
+ }
81
+ return String(val);
82
+ }
35
83
 
36
- node.on("input", function(msg, send, done) {
84
+ updateStaticStatus();
85
+
86
+ node.on("input", async function(msg, send, done) {
37
87
  send = send || function() { node.send.apply(node, arguments); };
38
88
 
39
89
  // Guard against invalid msg
@@ -43,7 +93,16 @@ module.exports = function(RED) {
43
93
  return;
44
94
  }
45
95
 
46
- updateStatus();
96
+ if (node.statusDisplay === "property" && node.statusProperty) {
97
+ const val = await resolveStatusProperty(msg);
98
+ if (val === undefined) {
99
+ utils.setStatusWarn(node, `${node.statusPropertyType === "jsonata" ? "expr" : node.statusProperty}: not found`);
100
+ } else {
101
+ utils.setStatusChanged(node, formatValue(val));
102
+ }
103
+ } else {
104
+ updateStaticStatus();
105
+ }
47
106
 
48
107
  send(msg);
49
108
  if (done) done();
@@ -6,6 +6,7 @@ module.exports = function(RED) {
6
6
  const node = this;
7
7
 
8
8
  node.contextPropertyName = config.contextPropertyName || "in1";
9
+ node.inputProperty = config.inputProperty || "payload";
9
10
  node.removeLabel = config.removeLabel || false;
10
11
 
11
12
  utils.setStatusOK(node, node.removeLabel ? "remove" : `set -> ${node.contextPropertyName}`);
@@ -23,10 +24,10 @@ module.exports = function(RED) {
23
24
  // Set or remove context property
24
25
  if (node.removeLabel) {
25
26
  delete msg.context;
26
- utils.setStatusChanged(node, `${msg.payload} -> removed`);
27
+ utils.setStatusChanged(node, `${msg[node.inputProperty]} -> removed`);
27
28
  } else {
28
29
  msg.context = node.contextPropertyName;
29
- utils.setStatusChanged(node, `${msg.payload} -> ${node.contextPropertyName}`);
30
+ utils.setStatusChanged(node, `${msg[node.inputProperty]} -> ${node.contextPropertyName}`);
30
31
  }
31
32
 
32
33
  send(msg);
@@ -287,7 +287,7 @@ module.exports = function(RED) {
287
287
  const outDisplay = output % 1 === 0 ? output : output.toFixed(2);
288
288
 
289
289
  // Update status and send output
290
- utils.setStatusOK(node, `${inDisplay} ${inUnit} → ${outDisplay} ${outUnit}`);
290
+ utils.setStatusOK(node, `${outDisplay} ${outUnit}`);
291
291
 
292
292
  msg.payload = output;
293
293
  send(msg);
@@ -31,7 +31,7 @@
31
31
  outputs: 1,
32
32
  inputLabels: ["input"],
33
33
  outputLabels: ["transition"],
34
- icon: "font-awesome/fa-exchange",
34
+ icon: "font-awesome/fa-bolt",
35
35
  paletteLabel: "edge",
36
36
  label: function() {
37
37
  return this.name || "edge";
@@ -49,7 +49,9 @@ Detects boolean state transitions (true-to-false or false-to-true).
49
49
  : payload (string | boolean) : Transition type (`"true-to-false"`, `"false-to-true"`) for `"algorithm"`, true for `"reset"`.
50
50
 
51
51
  ### Outputs
52
- : payload (boolean) : `true` when the specified transition occurs; `false` otherwise.
52
+ : *entire message* : The original input message is forwarded **only** when the specified transition occurs. No output otherwise.
53
+ : edge (boolean) : `true` is added to the message to flag that a transition was detected.
54
+ : payload : Preserved from the input message (unchanged).
53
55
 
54
56
  ### Properties
55
57
  : name (string) : Display name in editor.
@@ -58,8 +60,9 @@ Detects boolean state transitions (true-to-false or false-to-true).
58
60
 
59
61
  ### Details
60
62
  Detects transitions in boolean input (read from the configured **Input Property**, default: `msg.payload`) based on the configured `algorithm`.
61
- Outputs `msg.payload` as true only when the specified transition occurs (true-to-false or false-to-true).
63
+ Forwards the **entire original message** only when the specified transition occurs (true-to-false or false-to-true), with `msg.edge = true` added.
62
64
  No output on first input after reset, if no transition occurs, or for non-boolean inputs.
65
+ Because the full message passes through, this node can be used inline to conditionally gate messages based on boolean state changes while preserving all upstream properties.
63
66
 
64
67
  Configuration via `msg.context`:
65
68
  - `"algorithm"`: Sets transition type, no output.
@@ -103,10 +103,11 @@ module.exports = function(RED) {
103
103
  }
104
104
 
105
105
  if (isTransition) {
106
- utils.setStatusChanged(node, `in: ${currentValue}, out: true`);
107
- send({ payload: true });
106
+ utils.setStatusChanged(node, `true`);
107
+ msg.edge = true;
108
+ send(msg);
108
109
  } else {
109
- utils.setStatusUnchanged(node, `in: ${currentValue}, out: none`);
110
+ utils.setStatusUnchanged(node, `none`);
110
111
  }
111
112
 
112
113
  node.lastValue = currentValue;
@@ -54,7 +54,7 @@
54
54
  return "";
55
55
  }
56
56
  },
57
- icon: "font-awesome/fa-exchange",
57
+ icon: "font-awesome/fa-list-ol",
58
58
  paletteLabel: "enum switch",
59
59
  label: function() {
60
60
  const rules = JSON.parse(this.rules || "[]");
@@ -12,8 +12,8 @@
12
12
  <input type="text" id="node-input-property" style="width:70%;">
13
13
  </div>
14
14
  <div class="form-row">
15
- <label for="node-input-writePriority" title="Priority is evaluated for final value when using global-setter/getter nodes."><i class="fa fa-sort-numeric-asc"></i> Priority</label>
16
- <input type="text" id="node-input-writePriority" placeholder="single">
15
+ <label for="node-input-writePriority" title="Network priority slot (1-16) for this message. Network writes from external control systems use these slots."><i class="fa fa-sort-numeric-asc"></i> Network Priority</label>
16
+ <input type="text" id="node-input-writePriority" placeholder="16">
17
17
  <input type="hidden" id="node-input-writePriorityType">
18
18
  </div>
19
19
 
@@ -29,13 +29,13 @@
29
29
 
30
30
  <div class="form-row">
31
31
  <label>&nbsp;</label>
32
- <button type="button" id="node-btn-clear-priorities" class="editor-button" style="width: calc(70% - 3px);" title="Clear all 16 priority slots on the live node (does not affect the default value)">
33
- <i class="fa fa-eraser"></i> Clear All Priorities
32
+ <button type="button" id="node-btn-clear-priorities" class="editor-button" style="width: calc(70% - 3px);" title="Clear all 16 network priority slots on the live node (does not affect the default or fallback values)">
33
+ <i class="fa fa-eraser"></i> Clear Network Priorities
34
34
  </button>
35
35
  </div>
36
36
 
37
37
  <div class="form-tips">
38
- <b>Note:</b> This node writes to the selected <b>Priority</b>, manually, by msg or flow. The actual Global Variable value will be the highest active priority.
38
+ <b>Note:</b> This node writes to the <b>Network Priority</b> slot (1-16), manually, by msg or flow. The actual value is determined by the highest active source: Priorities 1-16 → Fallback → Default.
39
39
  </div>
40
40
  </script>
41
41
 
@@ -49,7 +49,7 @@
49
49
  property: { value: "payload", required: true },
50
50
  defaultValue: { value: "", required: true },
51
51
  defaultValueType: { value: "num", required: true },
52
- writePriority: { value: "default", required: true },
52
+ writePriority: { value: "fallback", required: true },
53
53
  writePriorityType: { value: "dropdown" }
54
54
  },
55
55
  inputs: 1,
@@ -101,7 +101,7 @@
101
101
  { value: "14", label: "14"},
102
102
  { value: "15", label: "15"},
103
103
  { value: "16", label: "16 (Network Logic)"},
104
- { value: "default", label: "default (fallback)"},
104
+ { value: "fallback", label: "fallback (Internal Logic)"},
105
105
  ]
106
106
  }, "msg", "flow"],
107
107
  typeField: "#node-input-writePriorityType"
@@ -141,12 +141,12 @@ Manage a global variable in a repeatable way.
141
141
  ### Inputs
142
142
  : payload (any) : Input payload is passed through unchanged.
143
143
  : property (string) : The input property where the value is taken from.
144
- : priority (number|string) : _Optional_. Overrides the configured Priority at runtime. Accepts `1`–`16` or `"default"`. For example, `msg.priority = 8` writes to priority slot 8.
145
- : context (string) : _Optional_. Tagged-input priority routing (matches priority-block conventions). Accepts `"priority1"`–`"priority16"`, `"default"`, or `"reload"`. When both `msg.priority` and `msg.context` are present, `msg.priority` takes precedence.
144
+ : priority (number|string) : _Optional_. Overrides the configured Network Priority at runtime. Accepts `1`–`16`. For example, `msg.priority = 8` writes to priority slot 8.
145
+ : context (string) : _Optional_. Tagged-input routing. Accepts `"fallback"` (writes to the fallback slot) or `"priority1"`–`"priority16"` (writes to network priority slots), or `"reload"` (reload state without writing). When both `msg.priority` and `msg.context` are present, `msg.priority` takes precedence.
146
146
  : units (string) : The units associated with the value, if any. Also supports nested units at `msg.<inputProperty>.units`.
147
147
 
148
148
  ### Outputs
149
- : payload (any) : Original payload.
149
+ : payload (any) : Original payload plus full state object (value, activePriority, all priority slots, fallback, metadata).
150
150
 
151
151
  ### Details
152
152
  Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
@@ -155,7 +155,12 @@ This node allows you to set a global variable in one place, and retrieve it else
155
155
 
156
156
  When this node is deleted or the flow is redeployed, it will automatically remove (prune) the variable from the selected Context Store.
157
157
 
158
- **Clear All Priorities** button (in editor): Resets all 16 priority slots to `null` on the live running node. The default value is preserved. The active value recalculates to the highest remaining priority (or falls back to default). The node must be deployed first.
158
+ **Priority Hierarchy**: The active value is determined by the highest active source:
159
+ 1. **Network Priorities (1-16)**: Used by external automation systems (e.g., BACnet, network service). Priority 16 is typical for network logic, 1 for life safety.
160
+ 2. **Fallback**: A dynamic input slot updated via `{context: "fallback", payload: value}`. Useful for internal logic to write a secondary value (e.g., persistent context setpoints).
161
+ 3. **Default**: A static configuration value. Always used as the ultimate fallback when all other sources are null/empty.
162
+
163
+ **Clear Network Priorities** button (in editor): Resets all 16 priority slots to `null` on the live running node. The fallback and default values are preserved. The active value recalculates based on the hierarchy. The node must be deployed first.
159
164
 
160
165
  ### Status
161
166
  - Green (dot): Configuration update
@@ -164,6 +169,8 @@ When this node is deleted or the flow is redeployed, it will automatically remov
164
169
  - Red (ring): Error
165
170
  - Yellow (ring): Warning
166
171
 
172
+ Status prefix: Empty = default active, `F` = fallback active, `P` = network priority active (e.g., `P3` = priority 3).
173
+
167
174
  ### References
168
175
  - [Node-RED Documentation](https://nodered.org/docs/)
169
176
  - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
@@ -24,6 +24,7 @@ module.exports = function(RED) {
24
24
  value: node.defaultValue,
25
25
  defaultValue: node.defaultValue,
26
26
  activePriority: "default",
27
+ fallback: null,
27
28
  units: null,
28
29
  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
30
  metadata: {
@@ -93,44 +94,63 @@ module.exports = function(RED) {
93
94
  node.isBusy = true;
94
95
 
95
96
  // Resolve write priority — three sources, in order of precedence:
96
- // 1. msg.priority (number 1-16 or "default") — explicit per-message override
97
- // 2. msg.context ("priority1"–"priority16" or "default") — tagged-input pattern (matches priority-block)
97
+ // 1. msg.priority (number 1-16) — explicit per-message override
98
+ // 2. msg.context ("priority1"–"priority16", "fallback", "reload") — tagged-input pattern
98
99
  // 3. Configured writePriority (dropdown / msg / flow typed-input)
99
100
  // Use local variable — never mutate node.writePriority so the config default is preserved across messages
100
101
  let activePrioritySlot = null;
102
+ let isFallbackWrite = false;
103
+
101
104
  try {
102
- if (msg.hasOwnProperty("priority") && (typeof msg.priority === "number" || typeof msg.priority === "string")) {
103
- // Source 1: msg.priority (direct number or "default") — skip objects (e.g. priority array from state)
104
- const mp = msg.priority;
105
- if (mp === "default") {
106
- activePrioritySlot = "default";
105
+ if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
106
+ // Check for special contexts first
107
+ const ctx = msg.context;
108
+ if (ctx === "fallback") {
109
+ isFallbackWrite = true;
110
+ } else if (ctx === "reload") {
111
+ // Handled separately below
112
+ activePrioritySlot = "reload";
107
113
  } else {
108
- const p = parseInt(mp, 10);
109
- if (isNaN(p) || p < 1 || p > 16) {
110
- node.isBusy = false;
111
- return utils.sendError(node, msg, done, `Invalid msg.priority: ${mp}`);
114
+ // Check for priority context
115
+ const priorityMatch = /^priority([1-9]|1[0-6])$/.exec(ctx);
116
+ if (priorityMatch) {
117
+ activePrioritySlot = priorityMatch[1];
112
118
  }
113
- activePrioritySlot = String(p);
119
+ // Unknown contexts leave activePrioritySlot null → falls to config
114
120
  }
115
- } else if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
116
- // Source 2: msg.context tagged-input ("priority8", "default", etc.)
117
- // "reload" is handled separately below skip it here
118
- const ctx = msg.context;
119
- const priorityMatch = /^priority([1-9]|1[0-6])$/.exec(ctx);
120
- if (priorityMatch) {
121
- activePrioritySlot = priorityMatch[1];
122
- } else if (ctx === "default") {
123
- activePrioritySlot = "default";
121
+ }
122
+
123
+ if (msg.hasOwnProperty("priority") && (typeof msg.priority === "number" || typeof msg.priority === "string")) {
124
+ // Source 1: msg.priority (direct number 1-16) — skip objects (e.g. priority array from state)
125
+ const mp = msg.priority;
126
+ const p = parseInt(mp, 10);
127
+ if (isNaN(p) || p < 1 || p > 16) {
128
+ node.isBusy = false;
129
+ return utils.sendError(node, msg, done, `Invalid msg.priority: ${mp} (must be 1-16)`);
124
130
  }
125
- // Other contexts (e.g. "reload") leave activePrioritySlot null → falls to config
131
+ activePrioritySlot = String(p);
132
+ isFallbackWrite = false; // msg.priority overrides fallback context
126
133
  }
127
134
 
128
- // Source 3: Fall back to configured typed-input when no message override matched
129
- if (activePrioritySlot === null) {
135
+ // Source 3: Fall back to configured writePriority only if no message override matched
136
+ if (!isFallbackWrite && activePrioritySlot === null) {
137
+ let configuredSlot;
130
138
  if (utils.requiresEvaluation(config.writePriorityType)) {
131
- activePrioritySlot = await utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg);
139
+ configuredSlot = await utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg);
140
+ } else {
141
+ configuredSlot = config.writePriority;
142
+ }
143
+ // Allow "fallback" as a configured value
144
+ if (configuredSlot === "fallback") {
145
+ isFallbackWrite = true;
132
146
  } else {
133
- activePrioritySlot = config.writePriority;
147
+ // Validate configured priority (must be 1-16)
148
+ const cp = parseInt(configuredSlot, 10);
149
+ if (isNaN(cp) || cp < 1 || cp > 16) {
150
+ node.isBusy = false;
151
+ return utils.sendError(node, msg, done, `Invalid configured writePriority: ${configuredSlot} (must be 1-16 or fallback)`);
152
+ }
153
+ activePrioritySlot = String(cp);
134
154
  }
135
155
  }
136
156
  } catch (err) {
@@ -147,12 +167,12 @@ module.exports = function(RED) {
147
167
  }
148
168
 
149
169
  // Handle Reload
150
- if (msg.context === "reload") {
170
+ if (activePrioritySlot === "reload") {
151
171
  RED.events.emit("bldgblocks:global:value-changed", { key: node.varName, store: node.storeName, data: state });
152
172
  await utils.setGlobalState(node, node.varName, node.storeName, state);
153
173
 
154
- prefix = state.activePriority === 'default' ? '' : 'P';
155
- const statusText = `reload: ${prefix}${state.activePriority}:${state.value}${state.units || ''}`;
174
+ const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
175
+ const statusText = `reload: ${activeLabel}:${state.value}${state.units || ''}`;
156
176
 
157
177
  return utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
158
178
  }
@@ -168,9 +188,9 @@ module.exports = function(RED) {
168
188
  return utils.sendError(node, msg, done, `msg.${node.inputProperty} not found or invalid property path`);
169
189
  }
170
190
 
171
- // Update State
172
- if (activePrioritySlot === 'default') {
173
- state.defaultValue = inputValue === null || inputValue === "null" ? node.defaultValue : inputValue;
191
+ // Update State: either fallback or priority slot
192
+ if (isFallbackWrite) {
193
+ state.fallback = inputValue === null || inputValue === "null" ? null : inputValue;
174
194
  } else {
175
195
  const priority = parseInt(activePrioritySlot, 10);
176
196
  if (isNaN(priority) || priority < 1 || priority > 16) {
@@ -180,26 +200,22 @@ module.exports = function(RED) {
180
200
  state.priority[activePrioritySlot] = inputValue;
181
201
  }
182
202
  }
183
-
184
- if (state.defaultValue === null || state.defaultValue === "null" || state.defaultValue === undefined) {
185
- state.defaultValue = node.defaultValue;
186
- }
187
203
 
188
- // Calculate Winner
204
+ // Calculate Winner (includes priorities 1-16, then fallback, then default)
189
205
  const { value, priority } = utils.getHighestPriority(state);
190
206
 
191
207
  // Check for change
192
208
  if (value === state.value && priority === state.activePriority) {
193
209
  // Ensure payload stays in sync with value
194
210
  state.payload = state.value;
195
- // Persist even when output unchanged — the priority array itself changed
211
+ // Persist even when output unchanged — the priority/fallback array itself changed
196
212
  await utils.setGlobalState(node, node.varName, node.storeName, state);
197
213
  if (node.storeName !== 'default') {
198
214
  await utils.setGlobalState(node, node.varName, 'default', state);
199
215
  }
200
- prefix = `${activePrioritySlot === 'default' ? '' : 'P'}`;
201
- const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
202
- const noChangeText = `no change: ${prefix}${activePrioritySlot}:${inputValue} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units || ''}`;
216
+ const writeSlotLabel = isFallbackWrite ? 'fallback' : `P${activePrioritySlot}`;
217
+ const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
218
+ const noChangeText = `no change: ${writeSlotLabel}:${inputValue} > active: ${activeLabel}:${state.value}${state.units || ''}`;
203
219
  utils.setStatusUnchanged(node, noChangeText);
204
220
  // Pass message through even if no context change
205
221
  send({ ...state });
@@ -237,9 +253,9 @@ module.exports = function(RED) {
237
253
  await utils.setGlobalState(node, node.varName, 'default', state);
238
254
  }
239
255
 
240
- prefix = `${activePrioritySlot === 'default' ? '' : 'P'}`;
241
- const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
242
- const statusText = `write: ${prefix}${activePrioritySlot}:${inputValue}${state.units || ''} > active: ${statePrefix}${state.activePriority}:${state.value}${state.units || ''}`;
256
+ const writeSlotLabel = isFallbackWrite ? 'fallback' : `P${activePrioritySlot}`;
257
+ const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
258
+ const statusText = `write: ${writeSlotLabel}:${inputValue}${state.units || ''} > active: ${activeLabel}:${state.value}${state.units || ''}`;
243
259
 
244
260
  RED.events.emit("bldgblocks:global:value-changed", {
245
261
  key: node.varName,
@@ -302,7 +318,8 @@ module.exports = function(RED) {
302
318
  store: targetNode.storeName,
303
319
  data: state
304
320
  });
305
- utils.setStatusOK(targetNode, `cleared: default:${state.value}`);
321
+ const activeLabel = state.activePriority === 'default' ? 'default' : (state.activePriority === 'fallback' ? 'fallback' : `P${state.activePriority}`);
322
+ utils.setStatusOK(targetNode, `cleared: active: ${activeLabel}:${state.value}`);
306
323
  targetNode.send({ ...state });
307
324
 
308
325
  res.status(200).json({ message: "Priorities cleared", value: state.value, activePriority: state.activePriority });
@@ -180,7 +180,7 @@ module.exports = function(RED) {
180
180
  { payload: newState === "below" }
181
181
  ];
182
182
 
183
- utils.setStatusChanged(node, `in: ${inputValue.toFixed(2)}, state: ${newState}`);
183
+ utils.setStatusChanged(node, `${inputValue.toFixed(2)} -> ${newState}`);
184
184
 
185
185
  node.state = newState;
186
186
  send(output);
@@ -110,8 +110,8 @@ module.exports = function(RED) {
110
110
  // Check if output value has changed
111
111
  const isUnchanged = outputValue === node.lastOutput;
112
112
  const statusShape = isUnchanged ? "ring" : "dot";
113
- utils.setStatusOK(node, `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`);
114
- if (statusShape === "ring") utils.setStatusUnchanged(node, `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`);
113
+ utils.setStatusOK(node, `${outputValue.toFixed(2)}`);
114
+ if (statusShape === "ring") utils.setStatusUnchanged(node, `${outputValue.toFixed(2)}`);
115
115
 
116
116
  if (!isUnchanged) {
117
117
  node.lastOutput = outputValue;
@@ -44,10 +44,10 @@ module.exports = function(RED) {
44
44
  return;
45
45
  }
46
46
  outputValue = -inputValue;
47
- statusText = `in: ${inputValue.toFixed(2)}, out: ${outputValue.toFixed(2)}`;
47
+ statusText = `${inputValue.toFixed(2)} -> ${outputValue.toFixed(2)}`;
48
48
  } else if (typeof inputValue === "boolean") {
49
49
  outputValue = !inputValue;
50
- statusText = `in: ${inputValue}, out: ${outputValue}`;
50
+ statusText = `${inputValue} -> ${outputValue}`;
51
51
  } else {
52
52
  utils.setStatusError(node, "Unsupported type");
53
53
  if (done) done();
@@ -104,8 +104,17 @@ module.exports = function(RED) {
104
104
  RED.events.emit("bldgblocks:network:point-update", pointUpdateData);
105
105
 
106
106
  // Passthrough
107
- const prefix = msg.activePriority === 'default' ? '' : 'P';
108
- const statusText = `Passthrough: ${prefix}${msg.activePriority}:${msg.value}${msg.units === null ? "" : ` ${msg.units}`}`;
107
+ let statusLabel;
108
+ if (msg.activePriority === 'default') {
109
+ statusLabel = 'default';
110
+ } else if (msg.activePriority === 'fallback') {
111
+ statusLabel = 'fallback';
112
+ } else if (typeof msg.activePriority === 'number' || /^\d+$/.test(msg.activePriority)) {
113
+ statusLabel = `P:${msg.activePriority}`;
114
+ } else {
115
+ statusLabel = String(msg.activePriority);
116
+ }
117
+ const statusText = `Passthrough: ${statusLabel}:${msg.value}${msg.units === null ? "" : ` ${msg.units}`}`;
109
118
  utils.sendSuccess(node, msg, done, statusText, node.pointId, "ring");
110
119
 
111
120
  } catch (err) {
@@ -43,16 +43,12 @@ module.exports = function(RED) {
43
43
  }
44
44
  }
45
45
 
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;
46
+ // Update Priority Logic (only allow priorities 1-16, no default)
47
+ const priority = parseInt(msg.priority, 10);
48
+ if (isNaN(priority) || priority < 1 || priority > 16) {
49
+ return utils.sendError(node, msg, done, `Invalid Priority: ${msg.priority} (must be 1-16)`, msg.pointId);
55
50
  }
51
+ state.priority[msg.priority] = newValue;
56
52
 
57
53
  // Calculate Winner
58
54
  const result = utils.getHighestPriority(state);
@@ -63,8 +59,8 @@ module.exports = function(RED) {
63
59
  // Save (Async) & Emit
64
60
  await utils.setGlobalState(node, path, store, state);
65
61
 
66
- const prefixReq = msg.priority === 'default' ? '' : 'P';
67
- const prefixAct = state.activePriority === 'default' ? '' : 'P';
62
+ const prefixReq = 'P';
63
+ const prefixAct = state.activePriority === 'default' ? '' : (state.activePriority === 'fallback' ? 'F' : 'P');
68
64
  const statusMsg = `Wrote: ${prefixReq}${msg.priority}:${newValue} > Active: ${prefixAct}${state.activePriority}:${state.value}`;
69
65
 
70
66
  msg = { ...state, status: null };
@@ -90,14 +90,7 @@ module.exports = function(RED) {
90
90
  // Ignore unknown context
91
91
  }
92
92
 
93
- // Validate input payload
94
- if (!msg.hasOwnProperty("payload")) {
95
- utils.setStatusError(node, "missing payload");
96
- send(msg);
97
- if (done) done();
98
- return;
99
- }
100
-
93
+ // Get input value from configured property
101
94
  let inputValue;
102
95
  try {
103
96
  inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
@@ -199,16 +199,16 @@ module.exports = function(RED) {
199
199
  }
200
200
  }
201
201
 
202
- // Fall back to default or fallback
202
+ // Fall through to fallback, then default (matching global-setter hierarchy)
203
203
  if (selectedValue === null) {
204
- if (defaultValue !== null) {
205
- selectedValue = defaultValue;
206
- activePriority = "default";
207
- selectedMessage = messages.default;
208
- } else if (fallbackValue !== null) {
204
+ if (fallbackValue !== null) {
209
205
  selectedValue = fallbackValue;
210
206
  activePriority = "fallback";
211
207
  selectedMessage = messages.fallback;
208
+ } else if (defaultValue !== null) {
209
+ selectedValue = defaultValue;
210
+ activePriority = "default";
211
+ selectedMessage = messages.default;
212
212
  }
213
213
  }
214
214