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

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.
Files changed (110) hide show
  1. package/nodes/accumulate-block.html +18 -8
  2. package/nodes/accumulate-block.js +39 -44
  3. package/nodes/add-block.html +1 -1
  4. package/nodes/add-block.js +18 -11
  5. package/nodes/alarm-collector.html +260 -0
  6. package/nodes/alarm-collector.js +292 -0
  7. package/nodes/alarm-config.html +129 -0
  8. package/nodes/alarm-config.js +126 -0
  9. package/nodes/alarm-service.html +96 -0
  10. package/nodes/alarm-service.js +142 -0
  11. package/nodes/analog-switch-block.js +25 -36
  12. package/nodes/and-block.js +44 -15
  13. package/nodes/average-block.js +46 -41
  14. package/nodes/boolean-switch-block.js +10 -28
  15. package/nodes/boolean-to-number-block.html +18 -5
  16. package/nodes/boolean-to-number-block.js +24 -16
  17. package/nodes/cache-block.js +24 -37
  18. package/nodes/call-status-block.html +91 -32
  19. package/nodes/call-status-block.js +398 -115
  20. package/nodes/changeover-block.html +5 -0
  21. package/nodes/changeover-block.js +167 -162
  22. package/nodes/comment-block.html +1 -1
  23. package/nodes/comment-block.js +14 -9
  24. package/nodes/compare-block.html +14 -4
  25. package/nodes/compare-block.js +23 -18
  26. package/nodes/contextual-label-block.html +5 -0
  27. package/nodes/contextual-label-block.js +6 -16
  28. package/nodes/convert-block.html +25 -39
  29. package/nodes/convert-block.js +31 -16
  30. package/nodes/count-block.html +11 -5
  31. package/nodes/count-block.js +34 -32
  32. package/nodes/delay-block.js +58 -53
  33. package/nodes/divide-block.js +43 -45
  34. package/nodes/edge-block.html +17 -10
  35. package/nodes/edge-block.js +43 -41
  36. package/nodes/enum-switch-block.js +6 -6
  37. package/nodes/frequency-block.html +6 -1
  38. package/nodes/frequency-block.js +64 -74
  39. package/nodes/global-getter.html +51 -15
  40. package/nodes/global-getter.js +43 -13
  41. package/nodes/global-setter.html +1 -1
  42. package/nodes/global-setter.js +40 -12
  43. package/nodes/history-buffer.html +96 -0
  44. package/nodes/history-buffer.js +461 -0
  45. package/nodes/history-collector.html +29 -1
  46. package/nodes/history-collector.js +37 -16
  47. package/nodes/history-config.html +13 -1
  48. package/nodes/history-service.html +84 -0
  49. package/nodes/history-service.js +52 -0
  50. package/nodes/hysteresis-block.html +5 -0
  51. package/nodes/hysteresis-block.js +13 -16
  52. package/nodes/interpolate-block.html +20 -2
  53. package/nodes/interpolate-block.js +39 -50
  54. package/nodes/join.html +78 -0
  55. package/nodes/join.js +78 -0
  56. package/nodes/latch-block.js +12 -14
  57. package/nodes/load-sequence-block.js +102 -110
  58. package/nodes/max-block.js +26 -26
  59. package/nodes/memory-block.js +57 -58
  60. package/nodes/min-block.js +26 -25
  61. package/nodes/minmax-block.js +35 -34
  62. package/nodes/modulo-block.js +45 -43
  63. package/nodes/multiply-block.js +43 -41
  64. package/nodes/negate-block.html +17 -7
  65. package/nodes/negate-block.js +25 -19
  66. package/nodes/network-point-read.html +128 -0
  67. package/nodes/network-point-read.js +230 -0
  68. package/nodes/{network-register.html → network-point-register.html} +94 -7
  69. package/nodes/{network-register.js → network-point-register.js} +18 -4
  70. package/nodes/network-point-write.html +149 -0
  71. package/nodes/network-point-write.js +222 -0
  72. package/nodes/network-service-bridge.html +131 -0
  73. package/nodes/network-service-bridge.js +376 -0
  74. package/nodes/network-service-read.html +81 -0
  75. package/nodes/{network-read.js → network-service-read.js} +4 -3
  76. package/nodes/{network-point-registry.html → network-service-registry.html} +19 -4
  77. package/nodes/{network-point-registry.js → network-service-registry.js} +7 -2
  78. package/nodes/network-service-write.html +89 -0
  79. package/nodes/{network-write.js → network-service-write.js} +3 -3
  80. package/nodes/nullify-block.js +13 -15
  81. package/nodes/on-change-block.html +17 -9
  82. package/nodes/on-change-block.js +49 -46
  83. package/nodes/oneshot-block.html +13 -10
  84. package/nodes/oneshot-block.js +57 -75
  85. package/nodes/or-block.js +44 -15
  86. package/nodes/pid-block.html +54 -4
  87. package/nodes/pid-block.js +459 -248
  88. package/nodes/priority-block.js +24 -35
  89. package/nodes/rate-limit-block.js +70 -72
  90. package/nodes/rate-of-change-block.html +33 -14
  91. package/nodes/rate-of-change-block.js +74 -62
  92. package/nodes/round-block.html +14 -9
  93. package/nodes/round-block.js +32 -25
  94. package/nodes/saw-tooth-wave-block.js +49 -76
  95. package/nodes/scale-range-block.html +12 -6
  96. package/nodes/scale-range-block.js +46 -39
  97. package/nodes/sine-wave-block.js +49 -57
  98. package/nodes/string-builder-block.js +6 -6
  99. package/nodes/subtract-block.js +38 -34
  100. package/nodes/thermistor-block.js +44 -44
  101. package/nodes/tick-tock-block.js +32 -32
  102. package/nodes/time-sequence-block.js +30 -42
  103. package/nodes/triangle-wave-block.js +49 -69
  104. package/nodes/tstat-block.js +34 -44
  105. package/nodes/units-block.html +90 -69
  106. package/nodes/units-block.js +22 -30
  107. package/nodes/utils.js +206 -3
  108. package/package.json +14 -6
  109. package/nodes/network-read.html +0 -56
  110. package/nodes/network-write.html +0 -65
@@ -21,7 +21,7 @@ module.exports = function(RED) {
21
21
 
22
22
  // Guard against invalid msg
23
23
  if (!msg) {
24
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
24
+ utils.setStatusError(node, "invalid message");
25
25
  if (done) done();
26
26
  return;
27
27
  }
@@ -34,7 +34,7 @@ module.exports = function(RED) {
34
34
  // Check busy lock
35
35
  if (node.isBusy) {
36
36
  // Update status to let user know they are pushing too fast
37
- node.status({ fill: "yellow", shape: "ring", text: "busy - dropped msg" });
37
+ utils.setStatusBusy(node, "busy - dropped msg");
38
38
  if (done) done();
39
39
  return;
40
40
  }
@@ -57,7 +57,7 @@ module.exports = function(RED) {
57
57
  matchAgainst = results[0];
58
58
 
59
59
  if (matchAgainst === undefined) {
60
- node.status({ fill: "red", shape: "ring", text: "property evaluation failed" });
60
+ utils.setStatusError(node, "property evaluation failed");
61
61
  if (done) done();
62
62
  return;
63
63
  }
@@ -97,7 +97,7 @@ module.exports = function(RED) {
97
97
 
98
98
  if (match) {
99
99
  matched = true;
100
- node.status({ fill: "blue", shape: "dot", text: `Matched: ${rule.value}` });
100
+ utils.setStatusChanged(node, `Matched: ${rule.value}`);
101
101
  }
102
102
  }
103
103
 
@@ -113,9 +113,9 @@ module.exports = function(RED) {
113
113
  send(messages);
114
114
 
115
115
  if (!matched && rules.length > 0) {
116
- node.status({ fill: "blue", shape: "ring", text: "No match" });
116
+ utils.setStatusUnchanged(node, "No match");
117
117
  } else if (rules.length === 0) {
118
- node.status({ fill: "yellow", shape: "ring", text: "No rules configured" });
118
+ utils.setStatusWarn(node, "No rules configured");
119
119
  }
120
120
 
121
121
  if (done) done();
@@ -4,6 +4,10 @@
4
4
  <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
5
5
  <input type="text" id="node-input-name" placeholder="Name">
6
6
  </div>
7
+ <div class="form-row">
8
+ <label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
9
+ <input type="text" id="node-input-inputProperty" placeholder="payload">
10
+ </div>
7
11
  </script>
8
12
 
9
13
  <!-- JavaScript Section: Registers the node and handles editor logic -->
@@ -12,7 +16,8 @@
12
16
  category: "bldgblocks control",
13
17
  color: "#301934",
14
18
  defaults: {
15
- name: { value: "" }
19
+ name: { value: "" },
20
+ inputProperty: { value: "payload" }
16
21
  },
17
22
  inputs: 1,
18
23
  outputs: 1,
@@ -1,39 +1,36 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function FrequencyBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
  const node = this;
5
7
 
6
- // Initialize runtime state
7
- node.runtime = {
8
- name: config.name || "",
9
- lastIn: false,
10
- lastEdge: 0,
11
- completeCycle: false,
12
- ppm: 0,
13
- pph: 0,
14
- ppd: 0,
15
- pulseHistory: [], // Array to store {start: timestamp, duration: ms}
16
- currentPulseStart: 0
17
- };
18
-
19
- node.status({
20
- fill: "green",
21
- shape: "dot",
22
- text: "awaiting first pulse"
23
- });
8
+ // Initialize state
9
+ node.name = config.name || "";
10
+ node.inputProperty = config.inputProperty || "payload";
11
+ node.lastIn = false;
12
+ node.lastEdge = 0;
13
+ node.completeCycle = false;
14
+ node.ppm = 0;
15
+ node.pph = 0;
16
+ node.ppd = 0;
17
+ node.pulseHistory = []; // Array to store {start: timestamp, duration: ms}
18
+ node.currentPulseStart = 0;
19
+
20
+ utils.setStatusOK(node, "awaiting first pulse");
24
21
 
25
22
  function calculateDutyCycle(now, currentInputValue) {
26
23
  const oneHourAgo = now - 3600000;
27
24
 
28
25
  // Clean up pulses older than 1 hour
29
- node.runtime.pulseHistory = node.runtime.pulseHistory.filter(pulse => {
26
+ node.pulseHistory = node.pulseHistory.filter(pulse => {
30
27
  return (pulse.start + pulse.duration) > oneHourAgo;
31
28
  });
32
29
 
33
30
  let totalOnTime = 0;
34
31
 
35
32
  // Sum all pulse durations within the last hour
36
- node.runtime.pulseHistory.forEach(pulse => {
33
+ node.pulseHistory.forEach(pulse => {
37
34
  const pulseEnd = pulse.start + pulse.duration;
38
35
  const effectiveStart = Math.max(pulse.start, oneHourAgo);
39
36
  const effectiveEnd = Math.min(pulseEnd, now);
@@ -43,8 +40,8 @@ module.exports = function(RED) {
43
40
  });
44
41
 
45
42
  // Add current ongoing pulse if active
46
- if (currentInputValue && node.runtime.currentPulseStart > 0) {
47
- const currentPulseTime = Math.max(node.runtime.currentPulseStart, oneHourAgo);
43
+ if (currentInputValue && node.currentPulseStart > 0) {
44
+ const currentPulseTime = Math.max(node.currentPulseStart, oneHourAgo);
48
45
  totalOnTime += (now - currentPulseTime);
49
46
  }
50
47
 
@@ -59,7 +56,7 @@ module.exports = function(RED) {
59
56
 
60
57
  // Guard against invalid message
61
58
  if (!msg) {
62
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
59
+ utils.setStatusError(node, "invalid message");
63
60
  if (done) done();
64
61
  return;
65
62
  }
@@ -67,46 +64,45 @@ module.exports = function(RED) {
67
64
  // Handle context updates
68
65
  if (msg.hasOwnProperty("context")) {
69
66
  if (!msg.hasOwnProperty("payload")) {
70
- node.status({ fill: "red", shape: "ring", text: "missing payload for reset" });
67
+ utils.setStatusError(node, "missing payload for reset");
71
68
  if (done) done();
72
69
  return;
73
70
  }
74
71
  if (msg.context === "reset") {
75
72
  if (typeof msg.payload !== "boolean") {
76
- node.status({ fill: "red", shape: "ring", text: "invalid reset" });
73
+ utils.setStatusError(node, "invalid reset");
77
74
  if (done) done();
78
75
  return;
79
76
  }
80
77
  if (msg.payload === true) {
81
- node.runtime.lastIn = false;
82
- node.runtime.lastEdge = 0;
83
- node.runtime.completeCycle = false;
84
- node.runtime.ppm = 0;
85
- node.runtime.pph = 0;
86
- node.runtime.ppd = 0;
87
- node.runtime.pulseHistory = [];
88
- node.runtime.currentPulseStart = 0;
89
- node.status({ fill: "green", shape: "dot", text: "reset" });
78
+ node.lastIn = false;
79
+ node.lastEdge = 0;
80
+ node.completeCycle = false;
81
+ node.ppm = 0;
82
+ node.pph = 0;
83
+ node.ppd = 0;
84
+ node.pulseHistory = [];
85
+ node.currentPulseStart = 0;
86
+ utils.setStatusOK(node, "reset");
90
87
  }
91
88
  if (done) done();
92
89
  return;
93
90
  } else {
94
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
91
+ utils.setStatusWarn(node, "unknown context");
95
92
  if (done) done("Unknown context");
96
93
  return;
97
94
  }
98
95
  }
99
96
 
100
97
  // Validate input payload
101
- if (!msg.hasOwnProperty("payload")) {
102
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
103
- if (done) done();
104
- return;
98
+ let inputValue;
99
+ try {
100
+ inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
101
+ } catch (err) {
102
+ inputValue = undefined;
105
103
  }
106
-
107
- const inputValue = msg.payload;
108
104
  if (typeof inputValue !== "boolean") {
109
- node.status({ fill: "red", shape: "ring", text: "invalid payload" });
105
+ utils.setStatusError(node, "invalid or missing input property");
110
106
  if (done) done();
111
107
  return;
112
108
  }
@@ -114,18 +110,18 @@ module.exports = function(RED) {
114
110
  const now = Date.now();
115
111
 
116
112
  // Track pulse edges for duty cycle
117
- if (inputValue && !node.runtime.lastIn) {
113
+ if (inputValue && !node.lastIn) {
118
114
  // Rising edge - start new pulse
119
- node.runtime.currentPulseStart = now;
120
- } else if (!inputValue && node.runtime.lastIn) {
115
+ node.currentPulseStart = now;
116
+ } else if (!inputValue && node.lastIn) {
121
117
  // Falling edge - record completed pulse
122
- if (node.runtime.currentPulseStart > 0) {
123
- const duration = now - node.runtime.currentPulseStart;
124
- node.runtime.pulseHistory.push({
125
- start: node.runtime.currentPulseStart,
118
+ if (node.currentPulseStart > 0) {
119
+ const duration = now - node.currentPulseStart;
120
+ node.pulseHistory.push({
121
+ start: node.currentPulseStart,
126
122
  duration: duration
127
123
  });
128
- node.runtime.currentPulseStart = 0;
124
+ node.currentPulseStart = 0;
129
125
  }
130
126
  }
131
127
 
@@ -134,21 +130,21 @@ module.exports = function(RED) {
134
130
 
135
131
  // Initialize output
136
132
  let output = {
137
- ppm: node.runtime.ppm,
138
- pph: node.runtime.pph,
139
- ppd: node.runtime.ppd,
133
+ ppm: node.ppm,
134
+ pph: node.pph,
135
+ ppd: node.ppd,
140
136
  dutyCycle: dutyData.dutyCycle.toFixed(2),
141
137
  onTime: dutyData.onTime
142
138
  };
143
139
 
144
140
  // Detect rising edge
145
- if (inputValue && !node.runtime.lastIn) {
141
+ if (inputValue && !node.lastIn) {
146
142
  // Rising edge: true and lastIn was false
147
- if (!node.runtime.completeCycle) {
148
- node.runtime.completeCycle = true;
143
+ if (!node.completeCycle) {
144
+ node.completeCycle = true;
149
145
  } else {
150
146
  // Compute period in minutes
151
- let periodMs = now - node.runtime.lastEdge;
147
+ let periodMs = now - node.lastEdge;
152
148
  let periodMin = periodMs / 60000;
153
149
  if (periodMin > 0.001) {
154
150
  // Minimum 0.6ms period (1000 pulses/sec)
@@ -161,29 +157,23 @@ module.exports = function(RED) {
161
157
  output.pph = 60000;
162
158
  output.ppd = 1440000;
163
159
  }
164
- node.runtime.ppm = output.ppm;
165
- node.runtime.pph = output.pph;
166
- node.runtime.ppd = output.ppd;
160
+ node.ppm = output.ppm;
161
+ node.pph = output.pph;
162
+ node.ppd = output.ppd;
167
163
  }
168
- node.runtime.lastEdge = now;
169
- node.runtime.completeCycle = true;
170
-
171
- node.status({
172
- fill: "blue",
173
- shape: "dot",
174
- text: `input: ${inputValue}, ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`
175
- });
164
+ node.lastEdge = now;
165
+ node.completeCycle = true;
166
+
167
+ const edgeText = `input: ${inputValue}, ppm: ${output.ppm.toFixed(2)}, pph: ${output.pph.toFixed(2)}, ppd: ${output.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`;
168
+ utils.setStatusChanged(node, edgeText);
176
169
  send({ payload: output });
177
170
  } else {
178
- node.status({
179
- fill: "blue",
180
- shape: "ring",
181
- text: `input: ${inputValue}, ppm: ${node.runtime.ppm.toFixed(2)}, pph: ${node.runtime.pph.toFixed(2)}, ppd: ${node.runtime.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`
182
- });
171
+ const noEdgeText = `input: ${inputValue}, ppm: ${node.ppm.toFixed(2)}, pph: ${node.pph.toFixed(2)}, ppd: ${node.ppd.toFixed(2)}, duty: ${output.dutyCycle}%`;
172
+ utils.setStatusUnchanged(node, noEdgeText);
183
173
  }
184
174
 
185
175
  // Update lastIn
186
- node.runtime.lastIn = inputValue;
176
+ node.lastIn = inputValue;
187
177
 
188
178
  if (done) done();
189
179
  });
@@ -22,7 +22,9 @@
22
22
 
23
23
  <div class="form-row">
24
24
  <label for="node-input-outputProperty"><i class="fa fa-arrow-right"></i> Output</label>
25
- <input type="text" id="node-input-outputProperty" placeholder="payload" style="width:70%;">
25
+ <input type="text" id="node-input-outputProperty" placeholder="payload">
26
+ <input type="hidden" id="node-input-outputPropertyType">
27
+ <input type="hidden" id="node-input-dropdownPath">
26
28
  </div>
27
29
 
28
30
  <div class="form-row">
@@ -34,7 +36,9 @@
34
36
  </div>
35
37
 
36
38
  <div class="form-tips">
37
- <b>Note:</b> Targeting by Node ID allows the source path to change without breaking this link.
39
+ <b>Note:</b> Source path may change without breaking this link. 'Reactive' events do not cause disk reads.
40
+ Setter nodes will always write, on change, to its configured store and 'default' to ensure data stays 'in memory' during operation (avoids disk IO hammering).
41
+ 'Manual' reads from 'default' store in memory first. Will fallback to 'persistent'.
38
42
  </div>
39
43
  </script>
40
44
 
@@ -46,6 +50,8 @@
46
50
  name: { value: "" },
47
51
  targetNode: { value: "", required: true },
48
52
  outputProperty: { value: "payload", required: true },
53
+ outputPropertyType: { value: "msg", required: true },
54
+ dropdownPath: { value: "", required: false },
49
55
  updates: { value: "always", required: true },
50
56
  detail: {value: "getObject", required: true }
51
57
  },
@@ -76,9 +82,11 @@
76
82
  if (displayPath.startsWith("#") && displayPath.includes(":")) {
77
83
  displayPath = displayPath.substring(displayPath.indexOf(":") + 1);
78
84
  }
85
+
79
86
  candidateNodes.push({
80
87
  value: n.id,
81
- label: displayPath + (n.name ? ` (${n.name})` : "")
88
+ label: displayPath + (n.name ? ` (${n.name})` : ""),
89
+ path: displayPath
82
90
  });
83
91
  }
84
92
  });
@@ -88,8 +96,34 @@
88
96
  $("#node-input-targetNode").typedInput({
89
97
  types: [{ value: "target", options: candidateNodes }]
90
98
  });
99
+
100
+ $("#node-input-outputProperty").typedInput({
101
+ default: "msg",
102
+ types: ["msg", "flow",
103
+ {
104
+ value: "dropdown",
105
+ options: [
106
+ { value: "sourceToFlow", label: "Source To Flow"}
107
+ ]
108
+ }],
109
+ typeField: "#node-input-outputPropertyType"
110
+ }).typedInput("type", node.outputPropertyType || "msg").typedInput("value", node.outputProperty);
111
+
112
+ function updateOutputValue() {
113
+ const currentType = $("#node-input-outputProperty").typedInput("type");
114
+
115
+ if (currentType === "dropdown" && node.outputProperty === "sourceToFlow") {
116
+ const selectedSourceId = $("#node-input-targetNode").val();
117
+ const selectedOption = candidateNodes.find(opt => opt.value === selectedSourceId);
118
+
119
+ if (selectedOption && selectedOption.path) {
120
+ $("#node-input-dropdownPath").val(selectedOption.path);
121
+ }
122
+ }
123
+ }
91
124
 
92
- $("#node-input-outputProperty").typedInput({ types: ['msg'] });
125
+ $("#node-input-targetNode").on("change", updateOutputValue);
126
+ $("#node-input-outputProperty").on("change", updateOutputValue);
93
127
 
94
128
  $("#node-config-find-source").on("click", function() {
95
129
  const selectedId = $("#node-input-targetNode").val();
@@ -101,32 +135,34 @@
101
135
  </script>
102
136
 
103
137
  <!-- Help Section -->
104
- <script type="text/markdown" data-help-name="global-setter">
105
- Manage a global variable in a repeatable way.
138
+ <script type="text/markdown" data-help-name="global-getter">
139
+ Retrieve a global variable from a single source location.
106
140
 
107
141
  ### Inputs
108
- Retreived object will overwrite msg.
142
+ : any : Triggers retrieval of the global variable. Input message is passed through unchanged unless Manual trigger is disabled.
109
143
 
110
144
  ### Outputs
111
- : payload (object) : The stored object with values and metadata.
145
+ : payload (object) : The retrieved global variable object with values and metadata.
112
146
 
113
147
  ### Details
114
- Global variables are meant to be retrieved in other places, this necessarily means managing the same string in multiple places.
148
+ Global variables are meant to be retrieved in multiple places, which necessitates managing the same path in multiple locations.
115
149
 
116
- This node allows you to get a global variable anywhere from one source, while supporting rename and deletion.
150
+ This node allows you to get a global variable anywhere by referencing one source node, while automatically supporting renames and deletions without breaking downstream nodes.
117
151
 
118
- The source can be found using the search button. The payload property can be specified and can be set to only the value for chaining.
152
+ The Source reference will automatically resolve even if the source global-setter is renamed or moved to a different flow tab.
119
153
 
120
- Reactive updates means every time the source is updated those nodes will receive the new value via event.
154
+ Trigger mode: Manual (On Input Only) reads from the in-memory 'default' store first, falling back to 'persistent' store. Reactive (On Input & Update) also triggers whenever the source variable is updated, without disk reads.
121
155
 
122
156
  ### Status
123
157
  - Green (dot): Configuration update
124
- - Blue (dot): State changed
158
+ - Blue (dot): State changed
125
159
  - Blue (ring): State unchanged
126
160
  - Red (ring): Error
127
161
  - Yellow (ring): Warning
128
162
 
129
163
  ### References
130
- - [Node-RED Documentation](https://nodered.org/docs/)
164
+ - [Node-RED Documentation](https://nodered.org/docs/)
131
165
  - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
132
- </script>
166
+ </script>
167
+
168
+
@@ -5,6 +5,7 @@ module.exports = function(RED) {
5
5
  const node = this;
6
6
  node.targetNodeId = config.targetNode;
7
7
  node.outputProperty = config.outputProperty || "payload";
8
+ node.dropdownPath = config.dropdownPath || "";
8
9
  node.updates = config.updates;
9
10
  node.detail = config.detail;
10
11
 
@@ -18,7 +19,7 @@ module.exports = function(RED) {
18
19
 
19
20
  // --- Output Helper ---
20
21
  function sendValue(storedObject, msgToReuse, done) {
21
- const msg = msgToReuse || {};
22
+ const msg = RED.util.cloneMessage(msgToReuse) || {};
22
23
 
23
24
  if (storedObject !== undefined && storedObject !== null) {
24
25
  // Check if this is our custom wrapper object
@@ -26,15 +27,39 @@ module.exports = function(RED) {
26
27
  if (node.detail === "getObject") {
27
28
  Object.assign(msg, storedObject);
28
29
  }
29
- RED.util.setMessageProperty(msg, node.outputProperty, storedObject.value);
30
+ if (config.outputPropertyType === "flow" || config.outputPropertyType === "dropdown") {
31
+ if (config.outputProperty === "sourceToFlow") {
32
+ node.context().flow.set(node.dropdownPath, storedObject.value);
33
+ } else {
34
+ node.context().flow.set(node.outputProperty, storedObject.value);
35
+ }
36
+ } else {
37
+ RED.util.setMessageProperty(msg, node.outputProperty, storedObject.value);
38
+ }
30
39
  } else {
31
40
  // Legacy/Raw values
32
- RED.util.setMessageProperty(msg, node.outputProperty, storedObject);
41
+ if (config.outputPropertyType === "flow" || config.outputPropertyType === "dropdown") {
42
+ if (config.outputProperty === "sourceToFlow") {
43
+ node.context().flow.set(node.dropdownPath, storedObject);
44
+ } else {
45
+ node.context().flow.set(node.outputProperty, storedObject);
46
+ }
47
+ } else {
48
+ RED.util.setMessageProperty(msg, node.outputProperty, storedObject);
49
+ }
33
50
  msg.metadata = { path: setterNode ? setterNode.varName : "unknown", legacy: true };
34
51
  }
35
52
 
36
- let valDisplay = RED.util.getMessageProperty(msg, node.outputProperty);
37
- valDisplay = typeof valDisplay === "number" ? valDisplay : valDisplay;
53
+ let valDisplay = storedObject.value;
54
+ if (valDisplay === null) valDisplay = "null";
55
+ else if (valDisplay === undefined) valDisplay = "undefined";
56
+ else if (typeof valDisplay === "object") valDisplay = JSON.stringify(valDisplay);
57
+ else valDisplay = typeof valDisplay === "number" ? valDisplay : valDisplay;
58
+
59
+ // Trim to 64 characters with ellipsis
60
+ if (valDisplay.length > 64) {
61
+ valDisplay = valDisplay.substring(0, 64) + "...";
62
+ }
38
63
 
39
64
  utils.sendSuccess(node, msg, done, `get: ${valDisplay}`, null, "dot");
40
65
  } else {
@@ -48,7 +73,7 @@ module.exports = function(RED) {
48
73
 
49
74
  if (setterNode && setterNode.varName && node.updates === 'always') {
50
75
  if (updateListener) {
51
- RED.events.removeListener("bldgblocks-global-update", updateListener);
76
+ RED.events.removeListener("bldgblocks:global:value-changed", updateListener);
52
77
  }
53
78
 
54
79
  updateListener = function(evt) {
@@ -58,14 +83,14 @@ module.exports = function(RED) {
58
83
  }
59
84
  };
60
85
 
61
- RED.events.on("bldgblocks-global-update", updateListener);
86
+ RED.events.on("bldgblocks:global:value-changed", updateListener);
62
87
 
63
88
  if (retryAction) {
64
89
  clearInterval(retryAction);
65
90
  retryAction = null;
66
91
  }
67
92
 
68
- node.status({ fill: "green", shape: "dot", text: "Connected" });
93
+ utils.setStatusOK(node, "Connected");
69
94
  return true;
70
95
  }
71
96
  return false;
@@ -73,12 +98,12 @@ module.exports = function(RED) {
73
98
 
74
99
  function startHealthCheck() {
75
100
  const check = () => {
76
- const listeners = RED.events.listeners("bldgblocks-global-update");
101
+ const listeners = RED.events.listeners("bldgblocks:global:value-changed");
77
102
  const hasOurListener = listeners.includes(updateListener);
78
103
  if (!hasOurListener) {
79
104
  node.warn("Event listener lost, reconnecting...");
80
105
  if (establishListener()) {
81
- node.status({ fill: "green", shape: "dot", text: "Reconnected" });
106
+ utils.setStatusOK(node, "Reconnected");
82
107
  }
83
108
  }
84
109
  setTimeout(check, 30000);
@@ -110,8 +135,13 @@ module.exports = function(RED) {
110
135
  setterNode ??= RED.nodes.getNode(node.targetNodeId);
111
136
 
112
137
  if (setterNode && setterNode.varName) {
113
- // Async Get
114
- const storedObject = await utils.getGlobalState(node, setterNode.varName, setterNode.storeName);
138
+ // Async Get - required default store to keep values in memory for polled getter nodes.
139
+ // 'persistent' for cross reboot storage.
140
+ let storedObject = await utils.getGlobalState(node, setterNode.varName, 'default');
141
+ if (!storedObject) {
142
+ // Fallback to persistent store if not found in default. Should not happen normally.
143
+ storedObject = await utils.getGlobalState(node, setterNode.varName, setterNode.storeName);
144
+ }
115
145
  sendValue(storedObject, msg, done);
116
146
  } else {
117
147
  node.warn("Source node not found or not configured.");
@@ -133,7 +163,7 @@ module.exports = function(RED) {
133
163
  if (healthCheckAction) clearInterval(healthCheckAction);
134
164
  if (retryAction) clearInterval(retryAction);
135
165
  if (removed && updateListener) {
136
- RED.events.removeListener("bldgblocks-global-update", updateListener);
166
+ RED.events.removeListener("bldgblocks:global:value-changed", updateListener);
137
167
  }
138
168
  done();
139
169
  });
@@ -5,7 +5,7 @@
5
5
  </div>
6
6
  <div class="form-row">
7
7
  <label for="node-input-path"><i class="fa fa-sitemap"></i> Global Path</label>
8
- <input type="text" id="node-input-path" style="width: 70%;" placeholder="furnace/outputs/heat">
8
+ <input type="text" id="node-input-path" placeholder="furnace/outputs/heat">
9
9
  </div>
10
10
  <div class="form-row">
11
11
  <label for="node-input-property"><i class="fa fa-ellipsis-h"></i> Input</label>