@bldgblocks/node-red-contrib-control 0.1.33 → 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 (113) 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 +74 -67
  41. package/nodes/global-setter.html +1 -1
  42. package/nodes/global-setter.js +168 -188
  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-point-register.js +126 -0
  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-service-read.js +58 -0
  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-service-write.js +83 -0
  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 +275 -3
  108. package/package.json +14 -6
  109. package/nodes/network-read.html +0 -56
  110. package/nodes/network-read.js +0 -59
  111. package/nodes/network-register.js +0 -161
  112. package/nodes/network-write.html +0 -64
  113. package/nodes/network-write.js +0 -126
@@ -1,23 +1,24 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function ModuloBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
  const node = this;
5
7
 
6
8
  // Initialize runtime state
7
- node.runtime = {
8
- name: config.name,
9
- slots: parseInt(config.slots),
10
- inputs: Array(parseInt(config.slots) || 2).fill(1),
11
- lastResult: null
12
- };
9
+ // Initialize state
10
+ node.name = config.name;
11
+ node.slots = parseInt(config.slots);
12
+ node.inputs = Array(parseInt(config.slots) || 2).fill(1);
13
+ node.lastResult = null;
13
14
 
14
15
  // Validate initial config
15
- if (isNaN(node.runtime.slots) || node.runtime.slots < 1) {
16
- node.runtime.slots = 2;
17
- node.runtime.inputs = Array(2).fill(1);
18
- node.status({ fill: "red", shape: "ring", text: "invalid slots, using 2" });
16
+ if (isNaN(node.slots) || node.slots < 1) {
17
+ node.slots = 2;
18
+ node.inputs = Array(2).fill(1);
19
+ utils.setStatusError(node, "invalid slots, using 2");
19
20
  } else {
20
- node.status({ fill: "green", shape: "dot", text: `name: ${node.runtime.name || "modulo"}, slots: ${node.runtime.slots}` });
21
+ utils.setStatusOK(node, `name: ${node.name || "modulo"}, slots: ${node.slots}`);
21
22
  }
22
23
 
23
24
  node.on("input", function(msg, send, done) {
@@ -25,35 +26,36 @@ module.exports = function(RED) {
25
26
 
26
27
  // Guard against invalid message
27
28
  if (!msg) {
28
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
29
+ utils.setStatusError(node, "invalid message");
29
30
  if (done) done();
30
31
  return;
31
32
  }
32
33
 
33
34
  // Check for missing context or payload
34
35
  if (!msg.hasOwnProperty("context") || typeof msg.context !== "string") {
35
- node.status({ fill: "red", shape: "ring", text: "missing context" });
36
+ utils.setStatusError(node, "missing context");
36
37
  if (done) done();
37
38
  return;
38
39
  }
39
40
 
40
41
  if (!msg.hasOwnProperty("payload")) {
41
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
42
+ utils.setStatusError(node, "missing payload");
42
43
  if (done) done();
43
44
  return;
44
45
  }
45
46
 
46
47
  // Handle configuration messages
47
48
  if (msg.context === "reset") {
48
- if (typeof msg.payload !== "boolean") {
49
- node.status({ fill: "red", shape: "ring", text: "invalid reset" });
49
+ const boolVal = utils.validateBoolean(msg.payload);
50
+ if (!boolVal.valid) {
51
+ utils.setStatusError(node, boolVal.error);
50
52
  if (done) done();
51
53
  return;
52
54
  }
53
- if (msg.payload === true) {
54
- node.runtime.inputs = Array(node.runtime.slots).fill(1);
55
- node.runtime.lastResult = null;
56
- node.status({ fill: "green", shape: "dot", text: "state reset" });
55
+ if (boolVal.value === true) {
56
+ node.inputs = Array(node.slots).fill(1);
57
+ node.lastResult = null;
58
+ utils.setStatusOK(node, "state reset");
57
59
  if (done) done();
58
60
  return;
59
61
  }
@@ -64,56 +66,56 @@ module.exports = function(RED) {
64
66
  if (msg.context === "slots") {
65
67
  const newSlots = parseInt(msg.payload);
66
68
  if (isNaN(newSlots) || newSlots < 1) {
67
- node.status({ fill: "red", shape: "ring", text: "invalid slots" });
69
+ utils.setStatusError(node, "invalid slots");
68
70
  if (done) done();
69
71
  return;
70
72
  }
71
- node.runtime.slots = newSlots;
72
- node.runtime.inputs = Array(newSlots).fill(1);
73
- node.runtime.lastResult = null;
74
- node.status({ fill: "green", shape: "dot", text: `slots: ${newSlots}` });
73
+ node.slots = newSlots;
74
+ node.inputs = Array(newSlots).fill(1);
75
+ node.lastResult = null;
76
+ utils.setStatusOK(node, `slots: ${newSlots}`);
75
77
  if (done) done();
76
78
  return;
77
79
  }
78
80
 
79
81
  if (msg.context.startsWith("in")) {
80
- const slotIndex = parseInt(msg.context.slice(2)) - 1;
81
- if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.runtime.slots) {
82
- node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
82
+ const slotVal = utils.validateSlotIndex(msg.context, node.slots);
83
+ if (!slotVal.valid) {
84
+ utils.setStatusError(node, slotVal.error);
83
85
  if (done) done();
84
86
  return;
85
87
  }
88
+ const slotIndex = slotVal.index - 1;
86
89
  const newValue = parseFloat(msg.payload);
87
90
  if (isNaN(newValue) || !isFinite(newValue)) {
88
- node.status({ fill: "red", shape: "ring", text: "invalid input" });
91
+ utils.setStatusError(node, "invalid input");
89
92
  if (done) done();
90
93
  return;
91
94
  }
92
95
  if (slotIndex > 0 && newValue === 0) {
93
- node.status({ fill: "red", shape: "ring", text: "modulo by zero" });
96
+ utils.setStatusError(node, "modulo by zero");
94
97
  if (done) done();
95
98
  return;
96
99
  }
97
- node.runtime.inputs[slotIndex] = newValue;
100
+ node.inputs[slotIndex] = newValue;
98
101
 
99
102
  // Calculate modulo
100
- const result = node.runtime.inputs.reduce((acc, val, idx) => idx === 0 ? val : acc % val, node.runtime.inputs[0]);
101
- const isUnchanged = result === node.runtime.lastResult;
102
- node.status({
103
- fill: "blue",
104
- shape: isUnchanged ? "ring" : "dot",
105
- text: `in: ${msg.context}=${newValue.toFixed(2)}, out: ${result.toFixed(2)}`
106
- });
107
-
108
- if (!isUnchanged) {
109
- node.runtime.lastResult = result;
110
- send({ payload: result });
103
+ const result = node.inputs.reduce((acc, val, idx) => idx === 0 ? val : acc % val, node.inputs[0]);
104
+ const isUnchanged = result === node.lastResult;
105
+ const statusText = `in: ${msg.context}=${newValue.toFixed(2)}, out: ${result.toFixed(2)}`;
106
+ if (isUnchanged) {
107
+ utils.setStatusUnchanged(node, statusText);
108
+ } else {
109
+ utils.setStatusChanged(node, statusText);
111
110
  }
111
+
112
+ node.lastResult = result;
113
+ send({ payload: result });
112
114
  if (done) done();
113
115
  return;
114
116
  }
115
117
 
116
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
118
+ utils.setStatusWarn(node, "unknown context");
117
119
  if (done) done("Unknown context");
118
120
  });
119
121
 
@@ -1,24 +1,25 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function MultiplyBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
 
5
7
  const node = this;
6
8
 
7
9
  // Initialize runtime state
8
- node.runtime = {
9
- name: config.name,
10
- slots: parseInt(config.slots),
11
- inputs: Array(parseInt(config.slots) || 2).fill(1),
12
- lastResult: null
13
- };
10
+ // Initialize state
11
+ node.name = config.name;
12
+ node.slots = parseInt(config.slots);
13
+ node.inputs = Array(parseInt(config.slots) || 2).fill(1);
14
+ node.lastResult = null;
14
15
 
15
16
  // Validate initial config
16
- if (isNaN(node.runtime.slots) || node.runtime.slots < 1) {
17
- node.runtime.slots = 2;
18
- node.runtime.inputs = Array(2).fill(1);
19
- node.status({ fill: "red", shape: "ring", text: "invalid slots, using 2" });
17
+ if (isNaN(node.slots) || node.slots < 1) {
18
+ node.slots = 2;
19
+ node.inputs = Array(2).fill(1);
20
+ utils.setStatusError(node, "invalid slots, using 2");
20
21
  } else {
21
- node.status({ fill: "green", shape: "dot", text: `name: ${node.runtime.name}, slots: ${node.runtime.slots}` });
22
+ utils.setStatusOK(node, `name: ${node.name}, slots: ${node.slots}`);
22
23
  }
23
24
 
24
25
  node.on("input", function(msg, send, done) {
@@ -26,81 +27,82 @@ module.exports = function(RED) {
26
27
 
27
28
  // Guard against invalid msg
28
29
  if (!msg) {
29
- node.status({ fill: "red", shape: "ring", text: "invalid message" });
30
+ utils.setStatusError(node, "invalid message");
30
31
  if (done) done();
31
32
  return;
32
33
  }
33
34
 
34
35
  // Check for missing context or payload
35
36
  if (!msg.hasOwnProperty("context")) {
36
- node.status({ fill: "red", shape: "ring", text: "missing context" });
37
+ utils.setStatusError(node, "missing context");
37
38
  if (done) done();
38
39
  return;
39
40
  }
40
41
 
41
42
  if (!msg.hasOwnProperty("payload")) {
42
- node.status({ fill: "red", shape: "ring", text: "missing payload" });
43
+ utils.setStatusError(node, "missing payload");
43
44
  if (done) done();
44
45
  return;
45
46
  }
46
47
 
47
48
  // Handle configuration messages
48
49
  if (msg.context === "reset") {
49
- if (typeof msg.payload !== "boolean") {
50
- node.status({ fill: "red", shape: "ring", text: "invalid reset" });
50
+ const boolVal = utils.validateBoolean(msg.payload);
51
+ if (!boolVal.valid) {
52
+ utils.setStatusError(node, boolVal.error);
51
53
  if (done) done();
52
54
  return;
53
55
  }
54
- if (msg.payload === true) {
55
- node.runtime.inputs = Array(node.runtime.slots).fill(1);
56
- node.runtime.lastResult = null;
57
- node.status({ fill: "green", shape: "dot", text: "state reset" });
56
+ if (boolVal.value === true) {
57
+ node.inputs = Array(node.slots).fill(1);
58
+ node.lastResult = null;
59
+ utils.setStatusOK(node, "state reset");
58
60
  if (done) done();
59
61
  return;
60
62
  }
61
63
  } else if (msg.context === "slots") {
62
64
  let newSlots = parseInt(msg.payload);
63
65
  if (isNaN(newSlots) || newSlots < 1) {
64
- node.status({ fill: "red", shape: "ring", text: "invalid slots" });
66
+ utils.setStatusError(node, "invalid slots");
65
67
  if (done) done();
66
68
  return;
67
69
  }
68
- node.runtime.slots = newSlots;
69
- node.runtime.inputs = Array(newSlots).fill(1);
70
- node.runtime.lastResult = null;
71
- node.status({ fill: "green", shape: "dot", text: `slots: ${node.runtime.slots}` });
70
+ node.slots = newSlots;
71
+ node.inputs = Array(newSlots).fill(1);
72
+ node.lastResult = null;
73
+ utils.setStatusOK(node, `slots: ${node.slots}`);
72
74
  if (done) done();
73
75
  return;
74
76
  } else if (msg.context.startsWith("in")) {
75
- let slotIndex = parseInt(msg.context.slice(2)) - 1;
76
- if (isNaN(slotIndex) || slotIndex < 0 || slotIndex >= node.runtime.slots) {
77
- node.status({ fill: "red", shape: "ring", text: `invalid input slot ${msg.context}` });
77
+ const slotVal = utils.validateSlotIndex(msg.context, node.slots);
78
+ if (!slotVal.valid) {
79
+ utils.setStatusError(node, slotVal.error);
78
80
  if (done) done();
79
81
  return;
80
82
  }
83
+ const slotIndex = slotVal.index - 1;
81
84
  let newValue = parseFloat(msg.payload);
82
85
  if (isNaN(newValue)) {
83
- node.status({ fill: "red", shape: "ring", text: "invalid input" });
86
+ utils.setStatusError(node, "invalid input");
84
87
  if (done) done();
85
88
  return;
86
89
  }
87
- node.runtime.inputs[slotIndex] = newValue;
90
+ node.inputs[slotIndex] = newValue;
88
91
  // Calculate product
89
- const product = node.runtime.inputs.reduce((acc, val) => acc * val, 1);
90
- const isUnchanged = product === node.runtime.lastResult;
91
- node.status({
92
- fill: "blue",
93
- shape: isUnchanged ? "ring" : "dot",
94
- text: `in: ${msg.context}=${newValue.toFixed(2)}, out: ${product.toFixed(2)}`
95
- });
96
- if (!isUnchanged) {
97
- node.runtime.lastResult = product;
98
- send({ payload: product });
92
+ const product = node.inputs.reduce((acc, val) => acc * val, 1);
93
+ const isUnchanged = product === node.lastResult;
94
+ const statusText = `in: ${msg.context}=${newValue.toFixed(2)}, out: ${product.toFixed(2)}`;
95
+ if (isUnchanged) {
96
+ utils.setStatusUnchanged(node, statusText);
97
+ } else {
98
+ utils.setStatusChanged(node, statusText);
99
99
  }
100
+ node.lastResult = product;
101
+ send({ payload: product });
100
102
  if (done) done();
101
103
  return;
102
104
  } else {
103
- node.status({ fill: "yellow", shape: "ring", text: "unknown context" });
105
+ utils.setStatusWarn(node, "unknown context");
104
106
  if (done) done();
105
107
  return;
106
108
  }
@@ -3,6 +3,10 @@
3
3
  <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
4
4
  <input type="text" id="node-input-name" placeholder="Name">
5
5
  </div>
6
+ <div class="form-row">
7
+ <label for="node-input-inputProperty" title="Message property to read input from"><i class="fa fa-folder-open"></i> Input Property</label>
8
+ <input type="text" id="node-input-inputProperty" placeholder="payload">
9
+ </div>
6
10
  </script>
7
11
 
8
12
  <script type="text/javascript">
@@ -10,7 +14,8 @@
10
14
  category: "bldgblocks control",
11
15
  color: "#301934",
12
16
  defaults: {
13
- name: { value: "" }
17
+ name: { value: "" },
18
+ inputProperty: { value: "payload" }
14
19
  },
15
20
  inputs: 1,
16
21
  outputs: 1,
@@ -28,17 +33,22 @@
28
33
  Negates a number or boolean input value.
29
34
 
30
35
  ### Inputs
31
- : payload (number | boolean) : Value to negate (number or boolean).
36
+ : input-property (number | boolean) : Value to negate (number or boolean), read from the configured Input Property.
32
37
 
33
38
  ### Outputs
34
39
  : payload (number | boolean) : Negated value (-number or !boolean).
35
40
 
36
- ### Details
37
- Negates `msg.payload` in a passthrough manner, updating the original message.
38
- - For numbers, outputs the negative (e.g., 5 becomes -5).
39
- - For booleans, outputs the inverse (e.g., true becomes false).
41
+ ### Properties
42
+ : name (string) : Display name in editor.
43
+ : inputProperty (string) : Message property to read input from (default: `payload`). Supports nested properties (e.g., `data.value`).
40
44
 
41
- Preserves other message properties (e.g., `msg.topic`, `msg.context`).
45
+ ### Details
46
+ Negates a number or boolean value in a passthrough manner, updating the original message.
47
+ - Reads input from the message property specified in the **Input Property** field (default: `msg.payload`)
48
+ - For numbers, outputs the negative (e.g., 5 becomes -5)
49
+ - For booleans, outputs the inverse (e.g., true becomes false)
50
+ - Output is always written to `msg.payload`
51
+ - Preserves other message properties (e.g., `msg.topic`, `msg.context`)
42
52
 
43
53
  ### Status
44
54
  - Green (dot): Configuration update
@@ -1,39 +1,45 @@
1
1
  module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
2
4
  function NegateBlockNode(config) {
3
5
  RED.nodes.createNode(this, config);
4
6
 
5
7
  const node = this;
6
8
 
7
- // Initialize runtime state
8
- node.runtime = {
9
- name: config.name,
10
- lastOutput: null
11
- };
9
+ // Initialize state
10
+ node.inputProperty = config.inputProperty || "payload";
11
+ node.lastOutput = null;
12
12
 
13
13
  node.on("input", function(msg, send, done) {
14
14
  send = send || function() { node.send.apply(node, arguments); };
15
15
 
16
16
  // Guard against invalid msg
17
17
  if (!msg) {
18
- node.status({ fill: "red", shape: "ring", text: "missing message" });
18
+ utils.setStatusError(node, "missing message");
19
19
  if (done) done();
20
20
  return;
21
21
  }
22
22
 
23
- // Check for missing payload
24
- if (!msg.hasOwnProperty("payload")) {
25
- node.status({ fill: "red", shape: "ring", text: "missing input" });
23
+ // Get input value from the specified property
24
+ let inputValue;
25
+ try {
26
+ inputValue = RED.util.getMessageProperty(msg, node.inputProperty);
27
+ } catch (err) {
28
+ inputValue = undefined;
29
+ }
30
+
31
+ if (inputValue === undefined) {
32
+ utils.setStatusError(node, "missing or invalid input property");
26
33
  if (done) done();
27
34
  return;
28
35
  }
29
36
 
30
- const inputValue = msg.payload;
31
37
  let outputValue;
32
38
  let statusText;
33
39
 
34
40
  if (typeof inputValue === "number") {
35
41
  if (isNaN(inputValue)) {
36
- node.status({ fill: "red", shape: "ring", text: "invalid input: NaN" });
42
+ utils.setStatusError(node, "invalid input: NaN");
37
43
  if (done) done();
38
44
  return;
39
45
  }
@@ -43,20 +49,20 @@ module.exports = function(RED) {
43
49
  outputValue = !inputValue;
44
50
  statusText = `in: ${inputValue}, out: ${outputValue}`;
45
51
  } else {
46
- node.status({ fill: "red", shape: "ring", text: "Unsupported type" });
52
+ utils.setStatusError(node, "Unsupported type");
47
53
  if (done) done();
48
54
  return;
49
55
  }
50
56
 
51
57
  // Check for unchanged output
52
- const isUnchanged = outputValue === node.runtime.lastOutput;
53
- node.status({
54
- fill: "blue",
55
- shape: isUnchanged ? "ring" : "dot",
56
- text: statusText
57
- });
58
+ const isUnchanged = outputValue === node.lastOutput;
59
+ if (isUnchanged) {
60
+ utils.setStatusUnchanged(node, statusText);
61
+ } else {
62
+ utils.setStatusChanged(node, statusText);
63
+ }
58
64
 
59
- node.runtime.lastOutput = outputValue;
65
+ node.lastOutput = outputValue;
60
66
  msg.payload = outputValue;
61
67
  send(msg);
62
68
 
@@ -0,0 +1,128 @@
1
+ <script type="text/html" data-template-name="network-point-read">
2
+ <div class="form-row">
3
+ <label for="node-input-name" title="Display name shown on canvas"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Point #101">
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-input-pointId" title="Point ID to cache (e.g., 301)"><i class="fa fa-hashtag"></i> Point ID</label>
8
+ <input type="number" id="node-input-pointId" placeholder="301" min="0" required>
9
+ </div>
10
+ <hr style="margin: 10px 0;">
11
+ <div class="form-row">
12
+ <label for="node-input-outputProperty" title="Set output message property"><i class="fa fa-arrow-right"></i> Target</label>
13
+ <input type="text" id="node-input-outputProperty" placeholder="payload">
14
+ </div>
15
+ <div class="form-row">
16
+ <label for="node-input-bridgeNodeId" title="Select which network bridge to query"><i class="fa fa-link"></i> Network Bridge</label>
17
+ <input type="text" id="node-input-bridgeNodeId" style="width: calc(70% - 45px);">
18
+ <button id="node-config-find-bridge" class="editor-button" style="margin-left: 5px; width: 40px;" title="Find Bridge Node">
19
+ <i class="fa fa-search"></i>
20
+ </button>
21
+ </div>
22
+ </script>
23
+
24
+ <script type="text/javascript">
25
+ RED.nodes.registerType("network-point-read", {
26
+ category: "bldgblocks network",
27
+ color: "#3090C7",
28
+ defaults: {
29
+ name: { value: "" },
30
+ outputProperty: { value: "payload" },
31
+ pointId: {
32
+ value: 0,
33
+ required: true,
34
+ validate: function(v) {
35
+ return !isNaN(parseInt(v)) && parseInt(v) >= 0;
36
+ }
37
+ },
38
+ bridgeNodeId: {
39
+ value: "",
40
+ required: true
41
+ }
42
+ },
43
+ inputs: 1,
44
+ outputs: 1,
45
+ inputLabels: ["trigger (from inject/timer)"],
46
+ outputLabels: ["value update / response"],
47
+ icon: "font-awesome/fa-refresh",
48
+ paletteLabel: "network point read",
49
+ label: function() {
50
+ const id = this.pointId ? ` #${this.pointId}` : " (unconfigured)";
51
+ const bridgeName = this.bridgeNodeId ? ` → ${RED.nodes.node(this.bridgeNodeId)?.name || "bridge"}` : "";
52
+ return this.name ? `${this.name}${id}${bridgeName}` : `network point read${id}${bridgeName}`;
53
+ },
54
+ oneditprepare: function() {
55
+ const node = this;
56
+
57
+ let candidateNodes = [];
58
+ RED.nodes.eachNode(function(n) {
59
+ if (n.type === 'network-service-bridge') {
60
+ candidateNodes.push({
61
+ value: n.id,
62
+ label: n.name || "(unnamed bridge)"
63
+ });
64
+ }
65
+ });
66
+
67
+ candidateNodes.sort((a, b) => a.label.localeCompare(b.label));
68
+
69
+ $("#node-input-bridgeNodeId").typedInput({
70
+ types: [{ value: "bridge", options: candidateNodes }]
71
+ });
72
+ $("#node-input-outputProperty").typedInput({
73
+ types: ['msg']
74
+ });
75
+
76
+
77
+ // Find button
78
+ $("#node-config-find-bridge").on("click", function() {
79
+ if (node.bridgeNodeId) {
80
+ RED.viewport.reveal(node.bridgeNodeId);
81
+ }
82
+ });
83
+ },
84
+ oneditsave: function() {
85
+ // Optional: log configuration
86
+ }
87
+ });
88
+ </script>
89
+
90
+ <script type="text/markdown" data-help-name="network-point-read">
91
+ Caches a remote point value and serves requests immediately (non-blocking).
92
+
93
+ Ideal for flows that need current point values without waiting for network latency.
94
+
95
+ ### Inputs
96
+ : action (string) : Command - `getPoint` to request cached value, `resetCache` to clear cache.
97
+ : pointId (number) : The remote point ID to query (e.g., 301).
98
+ : payload (any) : Trigger message from inject or timer node.
99
+
100
+ ### Outputs
101
+ : payload (number | string | boolean) : Cached point value on update.
102
+ : pointId (number) : Echo of queried point ID.
103
+ : cached (boolean) : true if value served from cache, false if fresh from network.
104
+ : age (number) : Cache age in milliseconds.
105
+ : action (string) : getPointResponse or pointUpdate.
106
+
107
+ ### Details
108
+ Caching Architecture:
109
+ 1. External trigger - Inject node controls polling timing
110
+ 2. Event-based communication - Sends request to selected bridge
111
+ 3. Immediate serving - Returns cached value on getPoint requests
112
+ 4. Non-blocking - Logic flows never wait for network roundtrip
113
+
114
+ Use With:
115
+ - network-service-bridge - Routes requests via WebSocket
116
+ - History nodes - Feed cached poi
117
+
118
+ ### Status
119
+ - Green (dot): Configuration update
120
+ - Blue (dot): State changed
121
+ - Blue (ring): State unchanged
122
+ - Red (ring): Error
123
+ - Yellow (ring): Warning
124
+
125
+ ### References
126
+ - [Node-RED Documentation](https://nodered.org/docs/)
127
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
128
+ </script>