@bldgblocks/node-red-contrib-control 0.2.1 → 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,64 @@ 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)
100
+ // Use local variable — never mutate node.writePriority so the config default is preserved across messages
101
+ let activePrioritySlot = null;
102
+ let isFallbackWrite = false;
103
+
99
104
  try {
100
- if (msg.hasOwnProperty("priority")) {
101
- // Source 1: msg.priority (direct number or "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";
113
+ } else {
114
+ // Check for priority context
115
+ const priorityMatch = /^priority([1-9]|1[0-6])$/.exec(ctx);
116
+ if (priorityMatch) {
117
+ activePrioritySlot = priorityMatch[1];
118
+ }
119
+ // Unknown contexts leave activePrioritySlot null → falls to config
120
+ }
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)
102
125
  const mp = msg.priority;
103
- if (mp === "default") {
104
- node.writePriority = "default";
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)`);
130
+ }
131
+ activePrioritySlot = String(p);
132
+ isFallbackWrite = false; // msg.priority overrides fallback context
133
+ }
134
+
135
+ // Source 3: Fall back to configured writePriority only if no message override matched
136
+ if (!isFallbackWrite && activePrioritySlot === null) {
137
+ let configuredSlot;
138
+ if (utils.requiresEvaluation(config.writePriorityType)) {
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;
105
146
  } else {
106
- const p = parseInt(mp, 10);
107
- if (isNaN(p) || p < 1 || p > 16) {
147
+ // Validate configured priority (must be 1-16)
148
+ const cp = parseInt(configuredSlot, 10);
149
+ if (isNaN(cp) || cp < 1 || cp > 16) {
108
150
  node.isBusy = false;
109
- return utils.sendError(node, msg, done, `Invalid msg.priority: ${mp}`);
151
+ return utils.sendError(node, msg, done, `Invalid configured writePriority: ${configuredSlot} (must be 1-16 or fallback)`);
110
152
  }
111
- node.writePriority = String(p);
153
+ activePrioritySlot = String(cp);
112
154
  }
113
- } else if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
114
- // Source 2: msg.context tagged-input ("priority8", "default", etc.)
115
- // "reload" is handled separately below — skip it here
116
- const ctx = msg.context;
117
- const priorityMatch = /^priority([1-9]|1[0-6])$/.exec(ctx);
118
- if (priorityMatch) {
119
- node.writePriority = priorityMatch[1];
120
- } else if (ctx === "default") {
121
- node.writePriority = "default";
122
- }
123
- // Other contexts (e.g. "reload") fall through — config stays as-is
124
- } else {
125
- // Source 3: Configured typed-input (dropdown, msg path, flow variable)
126
- const evaluations = [];
127
- evaluations.push(
128
- utils.requiresEvaluation(config.writePriorityType)
129
- ? utils.evaluateNodeProperty(config.writePriority, config.writePriorityType, node, msg)
130
- : Promise.resolve(node.writePriority)
131
- );
132
- const results = await Promise.all(evaluations);
133
- node.writePriority = results[0];
134
155
  }
135
156
  } catch (err) {
136
157
  throw new Error(`Property Eval Error: ${err.message}`);
@@ -146,12 +167,12 @@ module.exports = function(RED) {
146
167
  }
147
168
 
148
169
  // Handle Reload
149
- if (msg.context === "reload") {
170
+ if (activePrioritySlot === "reload") {
150
171
  RED.events.emit("bldgblocks:global:value-changed", { key: node.varName, store: node.storeName, data: state });
151
172
  await utils.setGlobalState(node, node.varName, node.storeName, state);
152
173
 
153
- prefix = state.activePriority === 'default' ? '' : 'P';
154
- 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 || ''}`;
155
176
 
156
177
  return utils.sendSuccess(node, { ...state }, done, statusText, null, "dot");
157
178
  }
@@ -167,38 +188,34 @@ module.exports = function(RED) {
167
188
  return utils.sendError(node, msg, done, `msg.${node.inputProperty} not found or invalid property path`);
168
189
  }
169
190
 
170
- // Update State
171
- if (node.writePriority === 'default') {
172
- 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;
173
194
  } else {
174
- const priority = parseInt(node.writePriority, 10);
195
+ const priority = parseInt(activePrioritySlot, 10);
175
196
  if (isNaN(priority) || priority < 1 || priority > 16) {
176
- return utils.sendError(node, msg, done, `Invalid priority: ${node.writePriority}`);
197
+ return utils.sendError(node, msg, done, `Invalid priority: ${activePrioritySlot}`);
177
198
  }
178
199
  if (inputValue !== undefined) {
179
- state.priority[node.writePriority] = inputValue;
200
+ state.priority[activePrioritySlot] = inputValue;
180
201
  }
181
202
  }
182
-
183
- if (state.defaultValue === null || state.defaultValue === "null" || state.defaultValue === undefined) {
184
- state.defaultValue = node.defaultValue;
185
- }
186
203
 
187
- // Calculate Winner
204
+ // Calculate Winner (includes priorities 1-16, then fallback, then default)
188
205
  const { value, priority } = utils.getHighestPriority(state);
189
206
 
190
207
  // Check for change
191
208
  if (value === state.value && priority === state.activePriority) {
192
209
  // Ensure payload stays in sync with value
193
210
  state.payload = state.value;
194
- // Persist even when output unchanged — the priority array itself changed
211
+ // Persist even when output unchanged — the priority/fallback array itself changed
195
212
  await utils.setGlobalState(node, node.varName, node.storeName, state);
196
213
  if (node.storeName !== 'default') {
197
214
  await utils.setGlobalState(node, node.varName, 'default', state);
198
215
  }
199
- prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
200
- const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
201
- const noChangeText = `no change: ${prefix}${node.writePriority}:${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 || ''}`;
202
219
  utils.setStatusUnchanged(node, noChangeText);
203
220
  // Pass message through even if no context change
204
221
  send({ ...state });
@@ -236,9 +253,9 @@ module.exports = function(RED) {
236
253
  await utils.setGlobalState(node, node.varName, 'default', state);
237
254
  }
238
255
 
239
- prefix = `${node.writePriority === 'default' ? '' : 'P'}`;
240
- const statePrefix = `${state.activePriority === 'default' ? '' : 'P'}`;
241
- const statusText = `write: ${prefix}${node.writePriority}:${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 || ''}`;
242
259
 
243
260
  RED.events.emit("bldgblocks:global:value-changed", {
244
261
  key: node.varName,
@@ -301,7 +318,8 @@ module.exports = function(RED) {
301
318
  store: targetNode.storeName,
302
319
  data: state
303
320
  });
304
- 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}`);
305
323
  targetNode.send({ ...state });
306
324
 
307
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}`;
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) {
@@ -29,6 +29,7 @@
29
29
  <tr style="border-bottom:2px solid #ccc;text-align:left;">
30
30
  <th style="padding:4px;width:28px;"><input type="checkbox" id="node-input-select-all" title="Select all"></th>
31
31
  <th style="padding:4px;width:70px;">ID</th>
32
+ <th style="padding:4px;width:30px;" title="Writable"><i class="fa fa-pencil"></i></th>
32
33
  <th style="padding:4px;">Path</th>
33
34
  <th style="padding:4px;width:30px;"></th>
34
35
  </tr>
@@ -106,7 +107,7 @@
106
107
  $selectAll.prop('checked', false);
107
108
 
108
109
  if (!points.length) {
109
- $tbody.append('<tr><td colspan="4" style="padding:8px;color:#999;">No points defined (deploy first if newly added)</td></tr>');
110
+ $tbody.append('<tr><td colspan="5" style="padding:8px;color:#999;">No points defined (deploy first if newly added)</td></tr>');
110
111
  $status.text("");
111
112
  return;
112
113
  }
@@ -141,6 +142,13 @@
141
142
 
142
143
  const $tdId = $('<td>').css({ padding: '3px 4px' }).append($idInput);
143
144
 
145
+ // Writable checkbox
146
+ const $wrCb = $('<input type="checkbox" class="point-writable">')
147
+ .prop('checked', !!pt.writable)
148
+ .attr('data-original-writable', pt.writable ? '1' : '0')
149
+ .attr('title', 'Allow network writes to this point');
150
+ const $tdWr = $('<td>').css({ padding: '3px 4px', textAlign: 'center' }).append($wrCb);
151
+
144
152
  // Path display
145
153
  const displayPath = pt.path && pt.path !== "not ready" ? pt.path : (pt.editorName || 'not deployed');
146
154
  const $tdPath = $('<td>').css({ padding: '3px 4px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '200px' })
@@ -157,7 +165,7 @@
157
165
  });
158
166
  const $tdBtn = $('<td>').css({ padding: '3px 4px' }).append($btn);
159
167
 
160
- $tr.append($tdCb, $tdId, $tdPath, $tdBtn);
168
+ $tr.append($tdCb, $tdId, $tdWr, $tdPath, $tdBtn);
161
169
  $tbody.append($tr);
162
170
  });
163
171
 
@@ -267,11 +275,25 @@
267
275
  changeCount++;
268
276
  }
269
277
  }
278
+
279
+ // Check writable change
280
+ const $wrCb = $(this).find('.point-writable');
281
+ const newWr = $wrCb.is(':checked');
282
+ const origWr = $wrCb.attr('data-original-writable') === '1';
283
+ if (newWr !== origWr) {
284
+ const editorNode = RED.nodes.node(nid);
285
+ if (editorNode) {
286
+ editorNode.writable = newWr;
287
+ editorNode.changed = true;
288
+ editorNode.dirty = true;
289
+ changeCount++;
290
+ }
291
+ }
270
292
  });
271
293
 
272
294
  if (changeCount > 0) {
273
295
  RED.nodes.dirty(true);
274
- RED.notify(changeCount + " point ID" + (changeCount !== 1 ? "s" : "") + " updated. Deploy to apply.", "success");
296
+ RED.notify(changeCount + " change" + (changeCount !== 1 ? "s" : "") + " applied. Deploy to take effect.", "success");
275
297
  }
276
298
  }
277
299
  });
@@ -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 };