@bldgblocks/node-red-contrib-control 0.1.25 → 0.1.27

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.
@@ -5,8 +5,8 @@
5
5
  <input type="text" id="node-input-name" placeholder="Name">
6
6
  </div>
7
7
  <div class="form-row">
8
- <label for="node-input-slots" title="Number of input slots (integer >= 1)"><i class="fa fa-th-list"></i> Slots</label>
9
- <input type="number" id="node-input-slots" placeholder="2" min="1" step="1">
8
+ <label for="node-input-slots" title="Number of input slots (integer >= 2)"><i class="fa fa-th-list"></i> Slots</label>
9
+ <input type="number" id="node-input-slots" placeholder="2" min="2" step="1">
10
10
  </div>
11
11
  </script>
12
12
 
@@ -17,7 +17,7 @@
17
17
  color: "#301934",
18
18
  defaults: {
19
19
  name: { value: "" },
20
- slots: { value: 2, required: true, validate: function(v) { return Number.isInteger(Number(v)) && Number(v) >= 1; } }
20
+ slots: { value: 2, required: true, validate: function(v) { return Number.isInteger(Number(v)) && Number(v) >= 2; } }
21
21
  },
22
22
  inputs: 1,
23
23
  outputs: 1,
@@ -36,19 +36,18 @@
36
36
  Selects one numeric input from multiple slots based on a switch value.
37
37
 
38
38
  ### Inputs
39
- : context (string) : Configures slots (`"slots"`), switch (`"switch"`), or input slot (`"inX"`, e.g., `in1`). Unmatched values trigger error.
40
- : payload (number | integer) : Number for input slot, integer for slots or switch configuration.
39
+ : context (string) : Switch (`"switch"`), or input slot (`"inX"`, e.g., `in1`). Unmatched values trigger error.
40
+ : payload (integer) : Switch configuration.
41
41
 
42
42
  ### Outputs
43
43
  : payload (number) : Selected input value based on switch setting.
44
44
 
45
45
  ### Properties
46
- : slots (integer) : Number of input slots (≥ 1). Default 2.
46
+ : slots (integer) : Number of input slots (≥ 2).
47
47
 
48
48
  ### Details
49
- Selects one numeric input from multiple slots (`in1`, `in2`, etc.) based on a switch value (1 to slots)
50
- set via `msg.context = "switch"` with integer `msg.payload`. Slots configurable via editor or `msg.context = "slots"` with
51
- integer `msg.payload` (≥ 1). Inputs default to 0, updated via `msg.context = "inX"` with numeric `msg.payload`.
49
+ Selects one numeric input from multiple slots (`in1`, `in2`, etc.) based on a switch value
50
+ set via `msg.context = "switch"` with integer `msg.payload`. Inputs default to 0, updated via `msg.context = "inX"` with numeric `msg.payload`.
52
51
 
53
52
  Outputs a new `msg.payload` when the active slot is updated, switch changes to a valid slot, or slots change affects the output.
54
53
 
@@ -39,29 +39,6 @@ module.exports = function(RED) {
39
39
  const prevSwitch = node.runtime.switch;
40
40
 
41
41
  switch (msg.context) {
42
- case "slots":
43
- const slotValue = parseInt(msg.payload, 10);
44
- if (isNaN(slotValue) || slotValue < 1) {
45
- node.status({ fill: "red", shape: "ring", text: "invalid slots" });
46
- if (done) done();
47
- return;
48
- }
49
- node.runtime.slots = slotValue;
50
- const newInputs = Array(node.runtime.slots).fill(0);
51
- for (let i = 0; i < Math.min(node.runtime.inputs.length, node.runtime.slots); i++) {
52
- newInputs[i] = node.runtime.inputs[i];
53
- }
54
- node.runtime.inputs = newInputs;
55
- if (node.runtime.switch > node.runtime.slots) {
56
- node.runtime.switch = 1;
57
- shouldOutput = true;
58
- }
59
- node.status({
60
- fill: "green",
61
- shape: "dot",
62
- text: `slots: ${node.runtime.slots}`
63
- });
64
- break;
65
42
  case "switch":
66
43
  const switchValue = parseInt(msg.payload, 10);
67
44
  if (isNaN(switchValue) || switchValue < 1 || switchValue > node.runtime.slots) {
@@ -61,7 +61,7 @@
61
61
  typeField: "#node-input-maxValidType"
62
62
  }).typedInput("type", node.maxValidType || "num").typedInput("value", node.maxValid || "150");
63
63
  } catch (err) {
64
- console.error("Error in hysteresis-block oneditprepare:", err);
64
+ console.error("Error in oneditprepare:", err);
65
65
  }
66
66
  }
67
67
  });
@@ -12,13 +12,10 @@ module.exports = function(RED) {
12
12
  lastAvg: null
13
13
  };
14
14
 
15
- const typedProperties = ['minValid', 'maxValid'];
16
-
17
15
  // Evaluate typed-input properties
18
16
  try {
19
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
20
- node.runtime.minValid = parseFloat(evaluatedValues.minValid);
21
- node.runtime.maxValid = parseFloat(evaluatedValues.maxValid);
17
+ node.runtime.minValid = parseFloat(RED.util.evaluateNodeProperty( config.minValid, config.minValidType, node ));
18
+ node.runtime.maxValid = parseFloat(RED.util.evaluateNodeProperty( config.maxValid, config.maxValidType, node ));
22
19
  } catch (err) {
23
20
  node.error(`Error evaluating properties: ${err.message}`);
24
21
  }
@@ -35,9 +32,12 @@ module.exports = function(RED) {
35
32
 
36
33
  // Update typed-input properties if needed
37
34
  try {
38
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
39
- node.runtime.minValid = parseFloat(evaluatedValues.minValid);
40
- node.runtime.maxValid = parseFloat(evaluatedValues.maxValid);
35
+ if (utils.requiresEvaluation(config.minValidType)) {
36
+ node.runtime.minValid = parseFloat(RED.util.evaluateNodeProperty( config.minValid, config.minValidType, node, msg ));
37
+ }
38
+ if (utils.requiresEvaluation(config.maxValidType)) {
39
+ node.runtime.maxValid = parseFloat(RED.util.evaluateNodeProperty( config.maxValid, config.maxValidType, node, msg ));
40
+ }
41
41
  } catch (err) {
42
42
  node.error(`Error evaluating properties: ${err.message}`);
43
43
  if (done) done();
@@ -1,3 +1,5 @@
1
+ //const { parse } = require('echarts/types/src/export/api/time.js');
2
+
1
3
  module.exports = function(RED) {
2
4
  const utils = require('./utils')(RED);
3
5
 
@@ -16,20 +18,16 @@ module.exports = function(RED) {
16
18
  lastModeChange: 0
17
19
  };
18
20
 
19
- const typedProperties = ['setpoint', 'heatingSetpoint', 'coolingSetpoint', 'swapTime', 'deadband',
20
- 'extent', 'minTempSetpoint', 'maxTempSetpoint'];
21
-
22
21
  // Evaluate typed-input properties
23
- try {
24
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
25
- node.runtime.setpoint = parseFloat(evaluatedValues.setpoint);
26
- node.runtime.heatingSetpoint = parseFloat(evaluatedValues.heatingSetpoint);
27
- node.runtime.coolingSetpoint = parseFloat(evaluatedValues.coolingSetpoint);
28
- node.runtime.swapTime = parseFloat(evaluatedValues.swapTime);
29
- node.runtime.deadband = parseFloat(evaluatedValues.deadband);
30
- node.runtime.extent = parseFloat(evaluatedValues.extent);
31
- node.runtime.minTempSetpoint = parseFloat(evaluatedValues.minTempSetpoint);
32
- node.runtime.maxTempSetpoint = parseFloat(evaluatedValues.maxTempSetpoint);
22
+ try {
23
+ node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node ));
24
+ node.runtime.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node ));
25
+ node.runtime.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node ));
26
+ node.runtime.swapTime = parseFloat(RED.util.evaluateNodeProperty( config.swapTime, config.swapTimeType, node ));
27
+ node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node ));
28
+ node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node ));
29
+ node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node ));
30
+ node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node ));
33
31
  } catch (err) {
34
32
  node.error(`Error evaluating properties: ${err.message}`);
35
33
  if (done) done();
@@ -53,15 +51,30 @@ module.exports = function(RED) {
53
51
 
54
52
  // Update typed-input properties if needed
55
53
  try {
56
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
57
- node.runtime.setpoint = parseFloat(evaluatedValues.setpoint);
58
- node.runtime.heatingSetpoint = parseFloat(evaluatedValues.heatingSetpoint);
59
- node.runtime.coolingSetpoint = parseFloat(evaluatedValues.coolingSetpoint);
60
- node.runtime.swapTime = parseFloat(evaluatedValues.swapTime);
61
- node.runtime.deadband = parseFloat(evaluatedValues.deadband);
62
- node.runtime.extent = parseFloat(evaluatedValues.extent);
63
- node.runtime.minTempSetpoint = parseFloat(evaluatedValues.minTempSetpoint);
64
- node.runtime.maxTempSetpoint = parseFloat(evaluatedValues.maxTempSetpoint);
54
+ if (utils.requiresEvaluation(config.setpointType)) {
55
+ node.runtime.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node, msg ));
56
+ }
57
+ if (utils.requiresEvaluation(config.heatingSetpointType)) {
58
+ node.runtime.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node, msg ));
59
+ }
60
+ if (utils.requiresEvaluation(config.coolingSetpointType)) {
61
+ node.runtime.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node, msg ));
62
+ }
63
+ if (utils.requiresEvaluation(config.swapTimeType)) {
64
+ node.runtime.swapTime = parseFloat(RED.util.evaluateNodeProperty( config.swapTime, config.swapTimeType, node, msg ));
65
+ }
66
+ if (utils.requiresEvaluation(config.deadbandType)) {
67
+ node.runtime.deadband = parseFloat(RED.util.evaluateNodeProperty( config.deadband, config.deadbandType, node, msg ));
68
+ }
69
+ if (utils.requiresEvaluation(config.extentType)) {
70
+ node.runtime.extent = parseFloat(RED.util.evaluateNodeProperty( config.extent, config.extentType, node, msg ));
71
+ }
72
+ if (utils.requiresEvaluation(config.minTempSetpointType)) {
73
+ node.runtime.minTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.minTempSetpoint, config.minTempSetpointType, node, msg ));
74
+ }
75
+ if (utils.requiresEvaluation(config.maxTempSetpointType)) {
76
+ node.runtime.maxTempSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.maxTempSetpoint, config.maxTempSetpointType, node, msg ));
77
+ }
65
78
  } catch (err) {
66
79
  node.error(`Error evaluating properties: ${err.message}`);
67
80
  if (done) done();
@@ -11,13 +11,10 @@ module.exports = function(RED) {
11
11
  desired: false
12
12
  };
13
13
 
14
- const typedProperties = ['delayOn', 'delayOff'];
15
-
16
14
  // Evaluate typed-input properties
17
15
  try {
18
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
19
- node.runtime.delayOn = (parseFloat(evaluatedValues.delayOn)) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
20
- node.runtime.delayOff = (parseFloat(evaluatedValues.delayOff)) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
16
+ node.runtime.delayOn = parseFloat(RED.util.evaluateNodeProperty( config.delayOn, config.delayOnType, node )) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
17
+ node.runtime.delayOff = parseFloat(RED.util.evaluateNodeProperty( config.delayOff, config.delayOffType, node )) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
21
18
  } catch (err) {
22
19
  node.error(`Error evaluating properties: ${err.message}`);
23
20
  }
@@ -33,9 +30,12 @@ module.exports = function(RED) {
33
30
 
34
31
  // Update typed-input properties if needed
35
32
  try {
36
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
37
- node.runtime.delayOn = parseFloat(evaluatedValues.delayOn) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
38
- node.runtime.delayOff = parseFloat(evaluatedValues.delayOff) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
33
+ if (utils.requiresEvaluation(config.delayOnType)) {
34
+ node.runtime.delayOn = parseFloat(RED.util.evaluateNodeProperty( config.delayOn, config.delayOnType, node, msg )) * (config.delayOnUnits === "seconds" ? 1000 : config.delayOnUnits === "minutes" ? 60000 : 1);
35
+ }
36
+ if (utils.requiresEvaluation(config.delayOffType)) {
37
+ node.runtime.delayOff = parseFloat(RED.util.evaluateNodeProperty( config.delayOff, config.delayOffType, node, msg )) * (config.delayOffUnits === "seconds" ? 1000 : config.delayOffUnits === "minutes" ? 60000 : 1);
38
+ }
39
39
  } catch (err) {
40
40
  node.error(`Error evaluating properties: ${err.message}`);
41
41
  if (done) done();
@@ -0,0 +1,157 @@
1
+ <script type="text/html" data-template-name="enum-switch-block">
2
+ <div class="form-row">
3
+ <label for="node-input-name" title="Display name shown on the canvas"><i class="fa fa-tag"></i> Name</label>
4
+ <input type="text" id="node-input-name" placeholder="Name">
5
+ </div>
6
+ <div class="form-row">
7
+ <label for="node-input-property" title="Property to evaluate"><i class="fa fa-code"></i> Property</label>
8
+ <input type="text" id="node-input-property" placeholder="property">
9
+ <input type="hidden" id="node-input-propertyType">
10
+ <input type="hidden" id="node-input-outputs">
11
+ </div>
12
+ <div class="form-row">
13
+ <label><i class="fa fa-cogs"></i> Rules</label>
14
+ <div id="node-input-rules-container" style="border: 1px solid #999; padding: 5px; margin: 5px 0; max-height: 200px; overflow-y: auto;">
15
+ <input type="hidden" id="node-input-rules">
16
+ <div class="rule-template" style="display: none;">
17
+ <div class="form-row rule-item" style="margin-bottom: 5px; padding: 5px; border-bottom: 1px solid #eee; display: flex; align-items: center;">
18
+ <input type="text" class="node-input-rule-value" placeholder="value to match" style="flex: 1; margin-right: 5px;">
19
+ <button class="red-ui-button delete-rule" style="width: auto; padding: 0 8px;">
20
+ <i class="fa fa-times"></i>
21
+ </button>
22
+ </div>
23
+ </div>
24
+ <div id="node-input-rules-list"></div>
25
+ <button id="node-input-add-rule" class="red-ui-button" style="width: 100%; margin-top: 5px;">
26
+ <i class="fa fa-plus"></i> Add Rule
27
+ </button>
28
+ </div>
29
+ </div>
30
+ </script>
31
+
32
+ <script type="text/javascript">
33
+ RED.nodes.registerType("enum-switch-block", {
34
+ category: "control",
35
+ color: "#301934",
36
+ defaults: {
37
+ name: { value: "" },
38
+ property: { value: "payload" },
39
+ propertyType: { value: "msg" },
40
+ rules: { value: "[]" },
41
+ outputs: { value: 1 }
42
+ },
43
+ inputs: 1,
44
+ outputs: 1,
45
+ inputLabels: ["input"],
46
+ outputLabels: function(index) {
47
+ try {
48
+ const rules = JSON.parse(this.rules || "[]");
49
+ if (rules[index]) {
50
+ return rules[index].value || `rule ${index + 1}`;
51
+ }
52
+ return "";
53
+ } catch (e) {
54
+ return "";
55
+ }
56
+ },
57
+ icon: "font-awesome/fa-exchange",
58
+ paletteLabel: "enum switch",
59
+ label: function() {
60
+ const rules = JSON.parse(this.rules || "[]");
61
+ return this.name ? this.name : `enum switch (${rules.length} rules)`;
62
+ },
63
+ oneditprepare: function() {
64
+ const node = this;
65
+ const rulesContainer = $("#node-input-rules-list");
66
+ const template = $(".rule-template").clone().removeClass("rule-template").show();
67
+
68
+ // Initialize typed inputs
69
+ $("#node-input-property").typedInput({
70
+ default: "msg",
71
+ types: ["msg", "flow", "global"],
72
+ typeField: "#node-input-propertyType"
73
+ }).typedInput("type", node.propertyType || "msg").typedInput("value", node.property);
74
+
75
+ // Parse existing rules
76
+ let rules = [];
77
+ try {
78
+ rules = JSON.parse(node.rules || "[]");
79
+ } catch (e) {
80
+ rules = [];
81
+ }
82
+
83
+ // Render existing rules
84
+ rules.forEach(function(rule, index) {
85
+ const ruleItem = template.clone();
86
+ ruleItem.find(".node-input-rule-value").val(rule.value || "");
87
+ rulesContainer.append(ruleItem);
88
+ });
89
+
90
+ // Add new rule button click
91
+ $("#node-input-add-rule").on("click", function() {
92
+ const ruleItem = template.clone();
93
+ rulesContainer.append(ruleItem);
94
+ });
95
+
96
+ // Delete rule button click
97
+ rulesContainer.on("click", ".delete-rule", function() {
98
+ $(this).closest(".rule-item").remove();
99
+ });
100
+ },
101
+
102
+ oneditsave: function() {
103
+ const rules = [];
104
+ $("#node-input-rules-list .rule-item").each(function() {
105
+ const value = $(this).find(".node-input-rule-value").val().trim();
106
+ if (value) {
107
+ rules.push({ value: value });
108
+ }
109
+ });
110
+
111
+ $("#node-input-rules").val(JSON.stringify(rules));
112
+ $("#node-input-outputs").val(rules.length);
113
+ },
114
+ oneditresize: function(size) {
115
+ $("#node-input-rules-container").height(size.height - 200);
116
+ }
117
+ });
118
+ </script>
119
+
120
+
121
+ <!-- Help Section -->
122
+ <script type="text/markdown" data-help-name="enum-switch-block">
123
+ Route input to the appropriate output based on switch values.
124
+
125
+ ### Inputs
126
+ : payload (string) : `string` for comparison.
127
+
128
+ ### Outputs
129
+ : payload (boolean) : Boolean output based on matching switch value.
130
+
131
+ ### Properties
132
+ : slots (integer) : Number of input slots (≥ 2).
133
+
134
+ ### Details
135
+ Similar to the NodeRED switch node, but single purpose, with the important difference:
136
+ 1. Outputting boolean based on matching string input.
137
+ 2. Updating all outputs `true`/`false` on each input message. (updating false being the key difference)
138
+
139
+ NodeRED `switch` node only outputs on matching cases, and does not update non-matching outputs unless you create opposing rules for each case,
140
+ which can be messy. Use case is to drive multiple boolean outputs based on a single string input, e.g., controlling visibility of multiple UI elements,
141
+ or routing logic based on a given mode.
142
+
143
+ Supports string, number, and boolean comparisons. Input type is inferred from the incoming message payload type.
144
+
145
+ Preserves original message structure, only modifying the payload to `true` or `false`.
146
+
147
+ ### Status
148
+ - Green (dot): Configuration update
149
+ - Blue (dot): State changed
150
+ - Blue (ring): State unchanged
151
+ - Red (ring): Error
152
+ - Yellow (ring): Warning
153
+
154
+ ### References
155
+ - [Node-RED Documentation](https://nodered.org/docs/)
156
+ - [GitHub Repository](https://github.com/BldgBlocks/node-red-contrib-buildingblocks-control.git)
157
+ </script>
@@ -0,0 +1,101 @@
1
+ module.exports = function(RED) {
2
+ const utils = require('./utils')(RED);
3
+
4
+ function EnumSwitchBlockNode(config) {
5
+ RED.nodes.createNode(this, config);
6
+ const node = this;
7
+
8
+ // Parse rules from config
9
+ let rules = [];
10
+ try {
11
+ rules = JSON.parse(config.rules || "[]");
12
+ } catch (e) {
13
+ node.error("Invalid rules configuration");
14
+ rules = [];
15
+ }
16
+
17
+ node.on("input", function(msg, send, done) {
18
+ send = send || function() { node.send.apply(node, arguments); };
19
+
20
+ // Guard against invalid msg
21
+ if (!msg) {
22
+ node.status({ fill: "red", shape: "ring", text: "invalid message" });
23
+ if (done) done();
24
+ return;
25
+ }
26
+
27
+ let matchAgainst;
28
+
29
+ // Evaluate typed-input properties
30
+ try {
31
+ matchAgainst = RED.util.evaluateNodeProperty( config.property, config.propertyType, node, msg );
32
+
33
+ if (matchAgainst === undefined) {
34
+ node.status({ fill: "red", shape: "ring", text: "property evaluation failed" });
35
+ if (done) done();
36
+ return;
37
+ }
38
+ } catch (err) {
39
+ node.status({ fill: "red", shape: "ring", text: `Error: ${err.message}` });
40
+ if (done) done(err);
41
+ return;
42
+ }
43
+
44
+ const outputs = [];
45
+ let matched = false;
46
+
47
+ // Evaluate each rule and set outputs
48
+ for (let i = 0; i < rules.length; i++) {
49
+ const rule = rules[i];
50
+ let match = false;
51
+
52
+ // Handle different types for comparison
53
+ if (matchAgainst === null || matchAgainst === undefined) {
54
+ match = (rule.value === null || rule.value === undefined || rule.value === "");
55
+ } else if (typeof matchAgainst === 'string' && typeof rule.value === 'string') {
56
+ match = matchAgainst === rule.value;
57
+ } else if (typeof matchAgainst === 'number') {
58
+ const numericRuleValue = parseFloat(rule.value);
59
+ match = !isNaN(numericRuleValue) && matchAgainst === numericRuleValue;
60
+ } else if (typeof matchAgainst === 'boolean') {
61
+ const boolRuleValue = rule.value.toLowerCase() === 'true';
62
+ match = matchAgainst === boolRuleValue;
63
+ } else {
64
+ match = String(matchAgainst) === String(rule.value);
65
+ }
66
+
67
+ outputs[i] = match;
68
+
69
+ if (match) {
70
+ matched = true;
71
+ node.status({ fill: "blue", shape: "dot", text: `Matched: ${rule.value}` });
72
+ }
73
+ }
74
+
75
+ // Send output messages (all outputs as booleans)
76
+ const messages = outputs.map(isMatch => {
77
+ return {
78
+ ...msg,
79
+ payload: isMatch,
80
+ topic: msg.topic
81
+ };
82
+ });
83
+
84
+ send(messages);
85
+
86
+ if (!matched && rules.length > 0) {
87
+ node.status({ fill: "blue", shape: "ring", text: "No match" });
88
+ } else if (rules.length === 0) {
89
+ node.status({ fill: "yellow", shape: "ring", text: "No rules configured" });
90
+ }
91
+
92
+ if (done) done();
93
+ });
94
+
95
+ node.on("close", function(done) {
96
+ if (done) done();
97
+ });
98
+ }
99
+
100
+ RED.nodes.registerType("enum-switch-block", EnumSwitchBlockNode);
101
+ };
@@ -94,7 +94,7 @@ Hysteresis controller with separate turn-on limits and turn-off differentials.
94
94
 
95
95
  ### Inputs
96
96
  : payload (number) : Input value to evaluate
97
- : context (string) : Configure `upperLimit`, `lowerLimit`, `upperLimitThreshold`, `lowerLimitThreshold`
97
+ : context (string) : Configure `upperLimitThreshold`, `lowerLimitThreshold`
98
98
 
99
99
  ### Outputs
100
100
  : above (boolean) : Input > upperLimit
@@ -7,15 +7,12 @@ module.exports = function(RED) {
7
7
  node.name = config.name;
8
8
  node.state = "within";
9
9
 
10
- const typedProperties = ['upperLimit', 'lowerLimit', 'upperLimitThreshold', 'lowerLimitThreshold'];
11
-
12
10
  // Evaluate typed-input properties
13
11
  try {
14
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
15
- node.upperLimit = parseFloat(evaluatedValues.upperLimit);
16
- node.lowerLimit = parseFloat(evaluatedValues.lowerLimit);
17
- node.upperLimitThreshold = parseFloat(evaluatedValues.upperLimitThreshold);
18
- node.lowerLimitThreshold = parseFloat(evaluatedValues.lowerLimitThreshold);
12
+ node.upperLimit = parseFloat(RED.util.evaluateNodeProperty( config.upperLimit, config.upperLimitType, node ));
13
+ node.lowerLimit = parseFloat(RED.util.evaluateNodeProperty( config.lowerLimit, config.lowerLimitType, node ));
14
+ node.upperLimitThreshold = parseFloat(RED.util.evaluateNodeProperty( config.upperLimitThreshold, config.upperLimitThresholdType, node ));
15
+ node.lowerLimitThreshold = parseFloat(RED.util.evaluateNodeProperty( config.lowerLimitThreshold, config.lowerLimitThresholdType, node ));
19
16
  } catch (err) {
20
17
  node.error(`Error evaluating properties: ${err.message}`);
21
18
  }
@@ -31,11 +28,18 @@ module.exports = function(RED) {
31
28
 
32
29
  // Update typed-input properties if needed
33
30
  try {
34
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
35
- node.upperLimit = parseFloat(evaluatedValues.upperLimit);
36
- node.lowerLimit = parseFloat(evaluatedValues.lowerLimit);
37
- node.upperLimitThreshold = parseFloat(evaluatedValues.upperLimitThreshold);
38
- node.lowerLimitThreshold = parseFloat(evaluatedValues.lowerLimitThreshold);
31
+ if (utils.requiresEvaluation(config.upperLimitType)) {
32
+ node.upperLimit = parseFloat(RED.util.evaluateNodeProperty( config.upperLimit, config.upperLimitType, node, msg ));
33
+ }
34
+ if (utils.requiresEvaluation(config.lowerLimitType)) {
35
+ node.lowerLimit = parseFloat(RED.util.evaluateNodeProperty( config.lowerLimit, config.lowerLimitType, node, msg ));
36
+ }
37
+ if (utils.requiresEvaluation(config.upperLimitThresholdType)) {
38
+ node.upperLimitThreshold = parseFloat(RED.util.evaluateNodeProperty( config.upperLimitThreshold, config.upperLimitThresholdType, node, msg ));
39
+ }
40
+ if (utils.requiresEvaluation(config.lowerLimitThresholdType)) {
41
+ node.lowerLimitThreshold = parseFloat(RED.util.evaluateNodeProperty( config.lowerLimitThreshold, config.lowerLimitThresholdType, node, msg ));
42
+ }
39
43
  } catch (err) {
40
44
  node.error(`Error evaluating properties: ${err.message}`);
41
45
  if (done) done();
@@ -12,8 +12,7 @@ module.exports = function(RED) {
12
12
 
13
13
  // Evaluate typed-inputs
14
14
  try {
15
- node.runtime.max = RED.util.evaluateNodeProperty( config.max, config.maxType, node );
16
- node.runtime.max = parseFloat(node.runtime.max);
15
+ node.runtime.max = parseFloat( RED.util.evaluateNodeProperty( config.max, config.maxType, node ));
17
16
  } catch(err) {
18
17
  node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
19
18
  }
@@ -34,8 +33,7 @@ module.exports = function(RED) {
34
33
  // Evaluate typed-inputs if needed
35
34
  try {
36
35
  if (utils.requiresEvaluation(config.maxType)) {
37
- node.runtime.max = RED.util.evaluateNodeProperty( config.max, config.maxType, node, msg );
38
- node.runtime.max = parseFloat(node.runtime.max);
36
+ node.runtime.max = parseFloat( RED.util.evaluateNodeProperty( config.max, config.maxType, node, msg ));
39
37
  }
40
38
  } catch(err) {
41
39
  node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
@@ -13,15 +13,13 @@ module.exports = function(RED) {
13
13
  node.runtime = {
14
14
  name: config.name,
15
15
  writePeriod: config.writePeriod,
16
- writePeriodType: config.writePeriodType,
17
16
  transferProperty: config.transferProperty,
18
17
  writeOnUpdate: config.writeOnUpdate === true,
19
18
  storedMsg: null
20
19
  };
21
20
 
22
21
  // Resolve typed inputs
23
- node.runtime.writePeriod = RED.util.evaluateNodeProperty( config.writePeriod, config.writePeriodType, node );
24
- node.runtime.writePeriod = parseFloat(node.runtime.writePeriod);
22
+ node.runtime.writePeriod = parseFloat(RED.util.evaluateNodeProperty( config.writePeriod, config.writePeriodType, node ));
25
23
 
26
24
  // File path for persistent storage
27
25
  const filePath = path.join(RED.settings.userDir, `memory-${node.id}.json`);
@@ -88,9 +86,8 @@ module.exports = function(RED) {
88
86
  }
89
87
 
90
88
  // Evaluate typed-inputs if needed
91
- if (utils.requiresEvaluation(node.runtime.writePeriodType)) {
92
- node.runtime.writePeriod = RED.util.evaluateNodeProperty( config.writePeriod, config.writePeriodType, node, msg );
93
- node.runtime.writePeriod = parseFloat(node.runtime.writePeriod);
89
+ if (utils.requiresEvaluation(config.writePeriodType)) {
90
+ node.runtime.writePeriod = parseFloat(RED.util.evaluateNodeProperty( config.writePeriod, config.writePeriodType, node, msg ));
94
91
  }
95
92
 
96
93
  // Initialize output array: [Output 1, Output 2]
@@ -12,8 +12,7 @@ module.exports = function(RED) {
12
12
 
13
13
  // Evaluate typed-inputs
14
14
  try {
15
- node.runtime.min = RED.util.evaluateNodeProperty( config.min, config.minType, node );
16
- node.runtime.min = parseFloat(node.runtime.min);
15
+ node.runtime.min = parseFloat(RED.util.evaluateNodeProperty( config.min, config.minType, node ));
17
16
  } catch(err) {
18
17
  node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
19
18
  }
@@ -34,8 +33,7 @@ module.exports = function(RED) {
34
33
  // Evaluate typed-inputs if needed
35
34
  try {
36
35
  if (utils.requiresEvaluation(config.minType)) {
37
- node.runtime.min = RED.util.evaluateNodeProperty( config.min, config.minType, node, msg );
38
- node.runtime.min = parseFloat(node.runtime.min);
36
+ node.runtime.min = parseFloat(RED.util.evaluateNodeProperty( config.min, config.minType, node, msg ));
39
37
  }
40
38
  } catch(err) {
41
39
  node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
@@ -9,14 +9,11 @@ module.exports = function(RED) {
9
9
  node.runtime = {
10
10
  name: config.name,
11
11
  };
12
-
13
- const typedProperties = ['min', 'max'];
14
12
 
15
13
  // Evaluate typed-input properties
16
14
  try {
17
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
18
- node.runtime.min = parseFloat(evaluatedValues.min);
19
- node.runtime.max = parseFloat(evaluatedValues.max);
15
+ node.runtime.min = parseFloat(RED.util.evaluateNodeProperty( config.min, config.minType, node ));
16
+ node.runtime.max = parseFloat(RED.util.evaluateNodeProperty( config.max, config.maxType, node ));
20
17
  } catch (err) {
21
18
  node.error(`Error evaluating properties: ${err.message}`);
22
19
  }
@@ -35,10 +32,13 @@ module.exports = function(RED) {
35
32
  }
36
33
 
37
34
  // Update typed-input properties if needed
38
- try {
39
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
40
- node.runtime.min = parseFloat(evaluatedValues.min);
41
- node.runtime.max = parseFloat(evaluatedValues.max);
35
+ try {
36
+ if (utils.requiresEvaluation(config.minType)) {
37
+ node.runtime.min = parseFloat(RED.util.evaluateNodeProperty( config.min, config.minType, node, msg ));
38
+ }
39
+ if (utils.requiresEvaluation(config.maxType)) {
40
+ node.runtime.max = parseFloat(RED.util.evaluateNodeProperty( config.max, config.maxType, node, msg ));
41
+ }
42
42
  } catch (err) {
43
43
  node.error(`Error evaluating properties: ${err.message}`);
44
44
  if (done) done();
@@ -46,7 +46,7 @@ Filters redundant messages based on value changes and a configurable period.
46
46
 
47
47
  ### Inputs
48
48
  : payload (any) : Input value to compare.
49
- : context (string, optional) : Configures period (`"period"`) or queries state (`"status"`). Unknown `msg.context` is ignored
49
+ : context (string, optional) : Configures period (`"period"`) or queries state (`"status"`).
50
50
  : payload (number | any, for `context`) : Non-negative number for `"period"` (ms), any for `"status"`.
51
51
 
52
52
  ### Outputs
@@ -9,14 +9,12 @@ module.exports = function(RED) {
9
9
  node.runtime = {
10
10
  name: config.name,
11
11
  lastValue: null,
12
- blockTimer: null,
13
- pendingMsg: null
12
+ blockTimer: null
14
13
  };
15
14
 
16
15
  // Evaluate typed-input properties
17
16
  try {
18
- node.runtime.period = RED.util.evaluateNodeProperty( node.runtime.period, node.runtime.periodType, node );
19
- node.runtime.period = parseFloat(node.runtime.period);
17
+ node.runtime.period = parseFloat(RED.util.evaluateNodeProperty( config.period, config.periodType, node ));
20
18
  } catch (err) {
21
19
  node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
22
20
  }
@@ -34,8 +32,7 @@ module.exports = function(RED) {
34
32
  // Evaluate typed-input properties if needed
35
33
  try {
36
34
  if (utils.requiresEvaluation(node.runtime.periodType)) {
37
- node.runtime.period = RED.util.evaluateNodeProperty( node.runtime.period, node.runtime.periodType, node, msg );
38
- node.runtime.period = parseFloat(node.runtime.period);
35
+ node.runtime.period = parseFloat(RED.util.evaluateNodeProperty( config.period, config.periodType, node, msg ));
39
36
  }
40
37
  } catch (err) {
41
38
  node.status({ fill: "red", shape: "ring", text: "error evaluating properties" });
@@ -45,14 +42,13 @@ module.exports = function(RED) {
45
42
 
46
43
  // Acceptable fallbacks
47
44
  if (isNaN(node.runtime.period) || node.runtime.period < 0) {
48
- node.runtime.period = 0;
45
+ node.runtime.period = config.period;
49
46
  node.status({ fill: "red", shape: "ring", text: "invalid period, using 0" });
50
47
  }
51
48
 
52
49
  // Handle context updates
53
50
  if (msg.hasOwnProperty("context") && typeof msg.context === "string") {
54
- const contextLower = msg.context.toLowerCase();
55
- if (contextLower === "period") {
51
+ if (msg.context === "period") {
56
52
  if (!msg.hasOwnProperty("payload")) {
57
53
  node.status({ fill: "red", shape: "ring", text: "missing payload for period" });
58
54
  if (done) done();
@@ -74,7 +70,7 @@ module.exports = function(RED) {
74
70
  if (done) done();
75
71
  return;
76
72
  }
77
- if (contextLower === "status") {
73
+ if (msg.context === "status") {
78
74
  send({
79
75
  payload: {
80
76
  period: node.runtime.period,
@@ -114,9 +110,8 @@ module.exports = function(RED) {
114
110
  return false;
115
111
  }
116
112
 
117
- // Handle input during filter period
113
+ // Block if in filter period
118
114
  if (node.runtime.blockTimer) {
119
- node.runtime.pendingMsg = RED.util.cloneMessage(msg);
120
115
  node.status({
121
116
  fill: "blue",
122
117
  shape: "ring",
@@ -126,49 +121,27 @@ module.exports = function(RED) {
126
121
  return;
127
122
  }
128
123
 
129
- // Allow no-change output if period > 0, othewise only on change
130
- if (!isEqual(currentValue, node.runtime.lastValue) || node.runtime.period > 0) {
131
- node.runtime.lastValue = RED.util.cloneMessage(currentValue);
132
- node.status({
133
- fill: "blue",
134
- shape: "dot",
135
- text: `out: ${JSON.stringify(currentValue).slice(0, 20)}`
136
- });
137
- send(msg);
138
-
139
- // Start filter period if applicable
140
- if (node.runtime.period > 0) {
141
- node.runtime.blockTimer = setTimeout(() => {
142
- node.runtime.blockTimer = null;
143
- if (node.runtime.pendingMsg) {
144
- const pendingValue = node.runtime.pendingMsg.payload;
145
- if (!isEqual(pendingValue, node.runtime.lastValue)) {
146
- node.runtime.lastValue = RED.util.cloneMessage(pendingValue);
147
- node.status({
148
- fill: "blue",
149
- shape: "dot",
150
- text: `out: ${JSON.stringify(pendingValue).slice(0, 20)}`
151
- });
152
- send(node.runtime.pendingMsg);
153
- } else {
154
- node.status({
155
- fill: "blue",
156
- shape: "ring",
157
- text: `Filter period expired`
158
- });
159
- }
160
- node.runtime.pendingMsg = null;
161
- } else {
162
- node.status({});
163
- }
164
- }, node.runtime.period);
124
+ // period === 0 means only ever on change, not equal outside of filter period sends an update message
125
+ if (isEqual(currentValue, node.runtime.lastValue)) {
126
+ if (node.runtime.period === 0) {
127
+ if (done) done();
128
+ return;
165
129
  }
166
- } else {
167
- node.status({
168
- fill: "blue",
169
- shape: "ring",
170
- text: `unchanged: ${JSON.stringify(currentValue).slice(0, 20)}`
171
- });
130
+ }
131
+
132
+ node.runtime.lastValue = currentValue;
133
+ send(msg);
134
+
135
+ // Start filter period if applicable
136
+ if (node.runtime.period > 0) {
137
+ node.runtime.blockTimer = setTimeout(() => {
138
+ node.runtime.blockTimer = null;
139
+ node.status({
140
+ fill: "blue",
141
+ shape: "ring",
142
+ text: `Filter period expired`
143
+ });
144
+ }, node.runtime.period);
172
145
  }
173
146
 
174
147
  if (done) done();
@@ -301,7 +301,7 @@ module.exports = function(RED) {
301
301
 
302
302
  // Output calculation
303
303
  let pv = pGain + intGain + dGain;
304
- if (node.runtime.directAction) pv = -pv;
304
+ //if (node.runtime.directAction) pv = -pv;
305
305
  pv = Math.min(Math.max(pv, node.runtime.outMin || -Infinity), node.runtime.outMax || Infinity);
306
306
 
307
307
  // Rate of change limit
@@ -10,24 +10,18 @@ module.exports = function(RED) {
10
10
  node.algorithm = config.algorithm;
11
11
  node.name = config.name;
12
12
 
13
- function shouldEvaluate(type) { return type === "flow" || type === "global" || type === "msg"; }
14
-
15
- const typedProperties = ['setpoint', 'heatingSetpoint', 'coolingSetpoint', 'coolingOn', 'coolingOff',
16
- 'heatingOff', 'heatingOn', 'diff', 'anticipator', 'ignoreAnticipatorCycles'];
17
-
18
13
  // Evaluate typed-input properties
19
14
  try {
20
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, null, true);
21
- node.setpoint = parseFloat(evaluatedValues.setpoint);
22
- node.heatingSetpoint = parseFloat(evaluatedValues.heatingSetpoint);
23
- node.coolingSetpoint = parseFloat(evaluatedValues.coolingSetpoint);
24
- node.coolingOn = parseFloat(evaluatedValues.coolingOn);
25
- node.coolingOff = parseFloat(evaluatedValues.coolingOff);
26
- node.heatingOff = parseFloat(evaluatedValues.heatingOff);
27
- node.heatingOn = parseFloat(evaluatedValues.heatingOn);
28
- node.diff = parseFloat(evaluatedValues.diff);
29
- node.anticipator = parseFloat(evaluatedValues.anticipator);
30
- node.ignoreAnticipatorCycles = Math.floor(evaluatedValues.ignoreAnticipatorCycles);
15
+ node.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node ));
16
+ node.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node ));
17
+ node.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node ));
18
+ node.coolingOn = parseFloat(RED.util.evaluateNodeProperty( config.coolingOn, config.coolingOnType, node ));
19
+ node.coolingOff = parseFloat(RED.util.evaluateNodeProperty( config.coolingOff, config.coolingOffType, node ));
20
+ node.heatingOff = parseFloat(RED.util.evaluateNodeProperty( config.heatingOff, config.heatingOffType, node ));
21
+ node.heatingOn = parseFloat(RED.util.evaluateNodeProperty( config.heatingOn, config.heatingOnType, node ));
22
+ node.diff = parseFloat(RED.util.evaluateNodeProperty( config.diff, config.diffType, node ));
23
+ node.anticipator = parseFloat(RED.util.evaluateNodeProperty( config.anticipator, config.anticipatorType, node ));
24
+ node.ignoreAnticipatorCycles = Math.floor(RED.util.evaluateNodeProperty( config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node ));
31
25
  } catch (err) {
32
26
  node.error(`Error evaluating properties: ${err.message}`);
33
27
  }
@@ -51,17 +45,36 @@ module.exports = function(RED) {
51
45
 
52
46
  // Update typed-input properties if needed
53
47
  try {
54
- const evaluatedValues = utils.evaluateProperties(node, config, typedProperties, msg);
55
- node.setpoint = parseFloat(evaluatedValues.setpoint);
56
- node.heatingSetpoint = parseFloat(evaluatedValues.heatingSetpoint);
57
- node.coolingSetpoint = parseFloat(evaluatedValues.coolingSetpoint);
58
- node.coolingOn = parseFloat(evaluatedValues.coolingOn);
59
- node.coolingOff = parseFloat(evaluatedValues.coolingOff);
60
- node.heatingOff = parseFloat(evaluatedValues.heatingOff);
61
- node.heatingOn = parseFloat(evaluatedValues.heatingOn);
62
- node.diff = parseFloat(evaluatedValues.diff);
63
- node.anticipator = parseFloat(evaluatedValues.anticipator);
64
- node.ignoreAnticipatorCycles = Math.floor(evaluatedValues.ignoreAnticipatorCycles);
48
+ if (utils.requiresEvaluation(config.setpointType)) {
49
+ node.setpoint = parseFloat(RED.util.evaluateNodeProperty( config.setpoint, config.setpointType, node, msg ));
50
+ }
51
+ if (utils.requiresEvaluation(config.heatingSetpointType)) {
52
+ node.heatingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.heatingSetpoint, config.heatingSetpointType, node, msg ));
53
+ }
54
+ if (utils.requiresEvaluation(config.coolingSetpointType)) {
55
+ node.coolingSetpoint = parseFloat(RED.util.evaluateNodeProperty( config.coolingSetpoint, config.coolingSetpointType, node, msg ));
56
+ }
57
+ if (utils.requiresEvaluation(config.coolingOnType)) {
58
+ node.coolingOn = parseFloat(RED.util.evaluateNodeProperty( config.coolingOn, config.coolingOnType, node, msg ));
59
+ }
60
+ if (utils.requiresEvaluation(config.coolingOffType)) {
61
+ node.coolingOff = parseFloat(RED.util.evaluateNodeProperty( config.coolingOff, config.coolingOffType, node, msg ));
62
+ }
63
+ if (utils.requiresEvaluation(config.heatingOffType)) {
64
+ node.heatingOff = parseFloat(RED.util.evaluateNodeProperty( config.heatingOff, config.heatingOffType, node, msg ));
65
+ }
66
+ if (utils.requiresEvaluation(config.heatingOnType)) {
67
+ node.heatingOn = parseFloat(RED.util.evaluateNodeProperty( config.heatingOn, config.heatingOnType, node, msg ));
68
+ }
69
+ if (utils.requiresEvaluation(config.diffType)) {
70
+ node.diff = parseFloat(RED.util.evaluateNodeProperty( config.diff, config.diffType, node, msg ));
71
+ }
72
+ if (utils.requiresEvaluation(config.anticipatorType)) {
73
+ node.anticipator = parseFloat(RED.util.evaluateNodeProperty( config.anticipator, config.anticipatorType, node, msg ));
74
+ }
75
+ if (utils.requiresEvaluation(config.ignoreAnticipatorCyclesType)) {
76
+ node.ignoreAnticipatorCycles = Math.floor(RED.util.evaluateNodeProperty( config.ignoreAnticipatorCycles, config.ignoreAnticipatorCyclesType, node, msg ));
77
+ }
65
78
  } catch (err) {
66
79
  node.error(`Error evaluating properties: ${err.message}`);
67
80
  if (done) done();
package/nodes/utils.js CHANGED
@@ -2,30 +2,9 @@ module.exports = function(RED) {
2
2
  function requiresEvaluation(type) { return type === "flow" || type === "global" || type === "msg"; }
3
3
 
4
4
 
5
- function evaluateProperties(node, config, properties, msg = null, initialize = true) {
6
- const results = {};
7
-
8
- properties.forEach(prop => {
9
- const type = config[`${prop}Type`];
10
- let value;
11
-
12
- if (type === "msg" && msg === null) {
13
- value = null;
14
- } else if (initialize || requiresEvaluation(type)) {
15
- value = RED.util.evaluateNodeProperty(config[prop], type, node, msg);
16
- } else {
17
- value = config[prop];
18
- }
19
- results[prop] = value;
20
- });
21
-
22
- return results;
23
- }
24
-
25
5
  // const utils = require('./utils')(RED);
26
6
 
27
7
  return {
28
- requiresEvaluation,
29
- evaluateProperties
8
+ requiresEvaluation
30
9
  };
31
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bldgblocks/node-red-contrib-control",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Sedona-inspired control nodes for Node-RED",
5
5
  "keywords": [ "node-red", "sedona", "control", "hvac" ],
6
6
  "files": ["nodes/*.js", "nodes/*.html"],
@@ -29,6 +29,7 @@
29
29
  "delay-block": "nodes/delay-block.js",
30
30
  "divide-block": "nodes/divide-block.js",
31
31
  "edge-block": "nodes/edge-block.js",
32
+ "enum-switch-block": "nodes/enum-switch-block.js",
32
33
  "frequency-block": "nodes/frequency-block.js",
33
34
  "hysteresis-block": "nodes/hysteresis-block.js",
34
35
  "interpolate-block": "nodes/interpolate-block.js",