node-red-contrib-boolean-logic-ultimate 1.2.13 → 1.2.14

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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,13 @@
4
4
 
5
5
  # CHANGELOG
6
6
 
7
+ <p>
8
+ <b>Version 1.2.14</b> June 2026<br/>
9
+
10
+ - NEW: MinMaxLimiterUltimate node, constrains a numeric value between a min and a max (clamp/saturation), e.g. to keep a dimmable lamp away from its extreme values.<br/>
11
+ - Replaced all node icons with native Node-RED icons for consistent, properly centered rendering.<br/>
12
+ </p>
13
+
7
14
  <p>
8
15
  <b>Version 1.2.13</b> May 2026<br/>
9
16
 
@@ -24,7 +24,7 @@
24
24
  break;
25
25
  }
26
26
  },
27
- icon: "light.png",
27
+ icon: "light.svg",
28
28
  label:
29
29
  function () {
30
30
  return (this.name || "Blinker") + " (" + this.blinkfrequency + "ms)";
@@ -56,7 +56,7 @@
56
56
  break;
57
57
  }
58
58
  },
59
- icon: "serial.png",
59
+ icon: "serial.svg",
60
60
  label:
61
61
  function () {
62
62
  let label = "";
@@ -21,7 +21,7 @@
21
21
  },
22
22
  inputs: 1,
23
23
  outputs: 1,
24
- icon: "font-awesome/fa-arrows-v",
24
+ icon: "sort.svg",
25
25
  label:
26
26
  function () {
27
27
  return this.name || "Comparator";
@@ -11,7 +11,7 @@
11
11
  inputs: 1,
12
12
  outputs: 1,
13
13
  outputLabels: ['Forward'],
14
- icon: 'file-in.png',
14
+ icon: 'file-in.svg',
15
15
  label: function () {
16
16
  return this.name || 'Debouncer';
17
17
  },
@@ -24,7 +24,7 @@
24
24
  break;
25
25
  }
26
26
  },
27
- icon: "switch.png",
27
+ icon: "switch.svg",
28
28
  label:
29
29
  function () {
30
30
  return this.name || "Filter";
@@ -20,7 +20,7 @@
20
20
  inputs: 1,
21
21
  outputs: 2,
22
22
  outputLabels: ['State output', 'Diagnostics'],
23
- icon: 'font-awesome/fa-sliders',
23
+ icon: 'rbe.svg',
24
24
  label: function () {
25
25
  return this.name || 'Hysteresis';
26
26
  },
@@ -134,4 +134,4 @@ Control topic messages:
134
134
  - `msg.state = true|false` to force state.
135
135
  - `msg.reset = true` to restore initial state.
136
136
  - `msg.status = true` to emit current status on output 2.
137
- </script>
137
+ </script>
@@ -25,7 +25,7 @@
25
25
  break;
26
26
  }
27
27
  },
28
- icon: "file-in.png",
28
+ icon: "file-in.svg",
29
29
  label:
30
30
  function () {
31
31
  return (this.name || "Interrupt") + " (" + this.triggertopic + ")";
@@ -11,7 +11,7 @@
11
11
  },
12
12
  inputs: 1,
13
13
  outputs: 1,
14
- icon: "swap.png",
14
+ icon: "swap.svg",
15
15
  label:
16
16
  function () {
17
17
  return this.name || "Invert";
@@ -13,7 +13,7 @@
13
13
  },
14
14
  inputs: 1,
15
15
  outputs: 1,
16
- icon: "swap.png",
16
+ icon: "swap.svg",
17
17
  label:
18
18
  function () {
19
19
  return this.name || "KalmanFilter";
@@ -0,0 +1,100 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('MinMaxLimiterUltimate', {
3
+ category: 'Boolean Logic Ultimate',
4
+ color: '#ff8080',
5
+ defaults: {
6
+ name: { value: '' },
7
+ controlTopic: { value: 'clamp' },
8
+ payloadPropName: { value: 'payload', required: false },
9
+ translatorConfig: { type: 'translator-config', required: false },
10
+ min: { value: 10, validate: RED.validators.number() },
11
+ max: { value: 90, validate: RED.validators.number() },
12
+ passInvalid: { value: false }
13
+ },
14
+ inputs: 1,
15
+ outputs: 1,
16
+ outputLabels: ['Clamped value'],
17
+ icon: 'range.svg',
18
+ label: function () {
19
+ return this.name || 'Min-Max Limiter';
20
+ },
21
+ paletteLabel: function () {
22
+ return 'Min-Max Limiter';
23
+ },
24
+ oneditprepare: function () {
25
+ const payloadField = $('#node-input-payloadPropName');
26
+ if (payloadField.val() === '') payloadField.val('payload');
27
+ payloadField.typedInput({ default: 'msg', types: ['msg'] });
28
+ }
29
+ });
30
+ </script>
31
+
32
+ <script type="text/html" data-template-name="MinMaxLimiterUltimate">
33
+ <div class="form-row">
34
+ <b>Min-Max Limiter</b>
35
+ &nbsp;&nbsp;<span style="color:red"><i class="fa fa-question-circle"></i>&nbsp;<a target="_blank" href="https://github.com/Supergiovane/node-red-contrib-boolean-logic-ultimate"><u>Help online</u></a></span>
36
+ </div>
37
+
38
+ <div class="form-row">
39
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
40
+ <input type="text" id="node-input-name" placeholder="Name">
41
+ </div>
42
+
43
+ <div class="form-row">
44
+ <label for="node-input-controlTopic"><i class="fa fa-tag"></i> Control topic</label>
45
+ <input type="text" id="node-input-controlTopic">
46
+ </div>
47
+
48
+ <div class="form-row">
49
+ <label for="node-input-min"><i class="fa fa-long-arrow-down"></i> Min (lower limit)</label>
50
+ <input type="number" id="node-input-min" step="any">
51
+ </div>
52
+
53
+ <div class="form-row">
54
+ <label for="node-input-max"><i class="fa fa-long-arrow-up"></i> Max (upper limit)</label>
55
+ <input type="number" id="node-input-max" step="any">
56
+ </div>
57
+
58
+ <div class="form-row">
59
+ <label for="node-input-passInvalid"><i class="fa fa-share"></i> Pass non-numbers</label>
60
+ <input type="checkbox" id="node-input-passInvalid" style="width:auto; margin-top:7px;">
61
+ </div>
62
+
63
+ <div class="form-row">
64
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> Input</label>
65
+ <input type="text" id="node-input-payloadPropName">
66
+ </div>
67
+
68
+ <div class="form-row">
69
+ <label for="node-input-translatorConfig"><i class="fa fa-language"></i> Translator</label>
70
+ <input type="text" id="node-input-translatorConfig">
71
+ </div>
72
+ </script>
73
+
74
+ <script type="text/markdown" data-help-name="MinMaxLimiterUltimate">
75
+ <p>Constrains a numeric value between a lower and an upper limit (clamp / saturation).
76
+ Useful, for example, to keep a dimmable lamp away from its extreme values.</p>
77
+
78
+ <p>If the input is below <code>Min</code> it outputs <code>Min</code>; if it is above
79
+ <code>Max</code> it outputs <code>Max</code>; otherwise the value passes through unchanged.</p>
80
+
81
+ |Property|Description|
82
+ |--|--|
83
+ | Control topic | Topic used to update limits at runtime. |
84
+ | Min | Lower limit. Values below it are raised to this value. |
85
+ | Max | Upper limit. Values above it are lowered to this value. |
86
+ | Pass non-numbers | If enabled, non-numeric inputs are forwarded unchanged instead of being dropped. |
87
+ | Input | Message property to read and write (default `payload`). |
88
+ | Translator | Optional translator-config. |
89
+
90
+ <p>The output message also carries <code>msg.clamp</code> with
91
+ <code>{ clamped, input, output, min, max }</code>.</p>
92
+
93
+ <p>Example (Min=10, Max=90): input <code>0</code> &rarr; <code>10</code>,
94
+ input <code>50</code> &rarr; <code>50</code>, input <code>100</code> &rarr; <code>90</code>.</p>
95
+
96
+ Control topic messages:
97
+
98
+ - `msg.min` to update the lower limit at runtime.
99
+ - `msg.max` to update the upper limit at runtime.
100
+ </script>
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (RED) {
4
+ const helpers = require('./lib/node-helpers.js');
5
+
6
+ function MinMaxLimiterUltimate(config) {
7
+ RED.nodes.createNode(this, config);
8
+ const node = this;
9
+ const REDUtil = RED.util;
10
+
11
+ const setNodeStatus = helpers.createStatus(node);
12
+
13
+ const controlTopic = config.controlTopic || 'clamp';
14
+ const payloadPropName = config.payloadPropName || 'payload';
15
+ const passInvalid = config.passInvalid === true;
16
+
17
+ let min = Number(config.min);
18
+ let max = Number(config.max);
19
+
20
+ if (!Number.isFinite(min)) min = 10;
21
+ if (!Number.isFinite(max)) max = 90;
22
+
23
+ function normalizeLimits() {
24
+ // Keep min <= max even if the user (or a control message) swaps them.
25
+ if (Number.isFinite(min) && Number.isFinite(max) && min > max) {
26
+ const tmp = min;
27
+ min = max;
28
+ max = tmp;
29
+ }
30
+ }
31
+
32
+ function toNumber(value) {
33
+ if (typeof value === 'number' && Number.isFinite(value)) {
34
+ return value;
35
+ }
36
+ if (typeof value === 'boolean') {
37
+ return value ? 1 : 0;
38
+ }
39
+ if (typeof value === 'string') {
40
+ const v = Number(value.trim());
41
+ return Number.isFinite(v) ? v : undefined;
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ function updateStatus(lastValue, clamped) {
47
+ let text;
48
+ if (lastValue === undefined) {
49
+ text = `min:${min} max:${max}`;
50
+ } else {
51
+ const valueText = Number(lastValue).toFixed(2).replace(/\.00$/, '');
52
+ text = `${valueText} [${min}..${max}]`;
53
+ }
54
+ setNodeStatus({
55
+ fill: clamped ? 'yellow' : 'green',
56
+ shape: clamped ? 'ring' : 'dot',
57
+ text,
58
+ });
59
+ }
60
+
61
+ function handleControl(msg) {
62
+ let consumed = false;
63
+
64
+ if (Object.prototype.hasOwnProperty.call(msg, 'min')) {
65
+ const next = Number(msg.min);
66
+ if (Number.isFinite(next)) {
67
+ min = next;
68
+ consumed = true;
69
+ }
70
+ }
71
+
72
+ if (Object.prototype.hasOwnProperty.call(msg, 'max')) {
73
+ const next = Number(msg.max);
74
+ if (Number.isFinite(next)) {
75
+ max = next;
76
+ consumed = true;
77
+ }
78
+ }
79
+
80
+ if (consumed) {
81
+ normalizeLimits();
82
+ updateStatus();
83
+ }
84
+
85
+ return consumed;
86
+ }
87
+
88
+ node.on('input', (msg) => {
89
+ if (msg.topic === controlTopic && handleControl(msg)) {
90
+ return;
91
+ }
92
+
93
+ const resolved = helpers.resolveInput(msg, payloadPropName, config.translatorConfig, RED);
94
+ const inputValue = toNumber(resolved.value);
95
+
96
+ if (inputValue === undefined) {
97
+ node.warn(`MinMaxLimiterUltimate: '${payloadPropName}' is not a number (${resolved.value})`);
98
+ updateStatus();
99
+ if (passInvalid) {
100
+ node.send(msg);
101
+ }
102
+ return;
103
+ }
104
+
105
+ let outputValue = inputValue;
106
+ if (outputValue < min) outputValue = min;
107
+ if (outputValue > max) outputValue = max;
108
+ const clamped = outputValue !== inputValue;
109
+
110
+ REDUtil.setMessageProperty(msg, payloadPropName, outputValue, true);
111
+ msg.clamp = {
112
+ clamped,
113
+ input: inputValue,
114
+ output: outputValue,
115
+ min,
116
+ max,
117
+ };
118
+
119
+ node.send(msg);
120
+ updateStatus(inputValue, clamped);
121
+ });
122
+
123
+ normalizeLimits();
124
+ updateStatus();
125
+ }
126
+
127
+ RED.nodes.registerType('MinMaxLimiterUltimate', MinMaxLimiterUltimate);
128
+ };
@@ -15,7 +15,7 @@
15
15
  },
16
16
  inputs: 1,
17
17
  outputs: 1,
18
- icon: 'file-in.png',
18
+ icon: 'file-in.svg',
19
19
  label: function () {
20
20
  return this.name || 'Presence Simulator';
21
21
  },
@@ -16,7 +16,7 @@
16
16
  outputLabels: function (i) {
17
17
  return "PIN " + i;
18
18
  },
19
- icon: "font-awesome/fa-train",
19
+ icon: "switch.svg",
20
20
  label:
21
21
  function () {
22
22
  return (this.name || "Switch") + " (" + this.triggertopic + ")";
@@ -22,7 +22,7 @@
22
22
  outputLabels: function (index) {
23
23
  return index === 0 ? 'Forward' : 'Diagnostics';
24
24
  },
25
- icon: 'file-in.png',
25
+ icon: 'file-in.svg',
26
26
  label: function () {
27
27
  const mode = (this.mode || 'debounce').charAt(0).toUpperCase() + (this.mode || 'debounce').slice(1);
28
28
  return (this.name || 'RateLimiter') + ' (' + mode + ')';
@@ -22,7 +22,7 @@
22
22
  break;
23
23
  }
24
24
  },
25
- icon: "font-awesome/fa-arrow-circle-o-right",
25
+ icon: "arrow-in.svg",
26
26
  label:
27
27
  function () {
28
28
  return this.name || "Simple Output";
@@ -22,7 +22,7 @@
22
22
  inputs: 1,
23
23
  outputs: 2,
24
24
  outputLabels: ['Light command', 'Warning'],
25
- icon: 'timer.png',
25
+ icon: 'timer.svg',
26
26
  label: function () {
27
27
  return this.name || 'Staircase Light';
28
28
  },
@@ -13,7 +13,7 @@
13
13
  },
14
14
  inputs: 1,
15
15
  outputs: 1,
16
- icon: "font-awesome/fa-eye",
16
+ icon: "white-globe.svg",
17
17
  label:
18
18
  function () {
19
19
  return this.name || "Status";
@@ -16,7 +16,7 @@
16
16
  },
17
17
  inputs: 1,
18
18
  outputs: 1,
19
- icon: "font-awesome/fa-plus",
19
+ icon: "hash.svg",
20
20
  label:
21
21
  function () {
22
22
  return this.name || "Math";
@@ -12,7 +12,7 @@
12
12
  },
13
13
  inputs: 1,
14
14
  outputs: 1,
15
- icon: "serial.png",
15
+ icon: "serial.svg",
16
16
  label:
17
17
  function () {
18
18
  return this.name || "Toggle";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-boolean-logic-ultimate",
3
- "version": "1.2.13",
3
+ "version": "1.2.14",
4
4
  "description": "A set of Node-RED enhanced boolean logic and utility nodes, flow interruption, blinker, debouncer, invert, filter, toggle etc.., with persistent values after reboot. Compatible also with Homeassistant values.",
5
5
  "author": "Supergiovane (https://github.com/Supergiovane)",
6
6
  "dependencies": {
@@ -47,6 +47,7 @@
47
47
  "PresenceSimulatorUltimate": "boolean-logic-ultimate/PresenceSimulatorUltimate.js",
48
48
  "StaircaseLightUltimate": "boolean-logic-ultimate/StaircaseLightUltimate.js",
49
49
  "HysteresisUltimate": "boolean-logic-ultimate/HysteresisUltimate.js",
50
+ "MinMaxLimiterUltimate": "boolean-logic-ultimate/MinMaxLimiterUltimate.js",
50
51
  "translator-config": "boolean-logic-ultimate/translator-config.js"
51
52
  }
52
53
  },
@@ -0,0 +1,128 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const { helper } = require('./helpers');
5
+
6
+ const clampNode = require('../boolean-logic-ultimate/MinMaxLimiterUltimate.js');
7
+
8
+ function loadClamp(flow, credentials) {
9
+ const normalizedFlow = flow.map((node, index) => {
10
+ if (
11
+ node &&
12
+ node.type &&
13
+ node.type !== 'tab' &&
14
+ node.type !== 'subflow' &&
15
+ node.type !== 'group' &&
16
+ node.z &&
17
+ !(Object.prototype.hasOwnProperty.call(node, 'x') && Object.prototype.hasOwnProperty.call(node, 'y'))
18
+ ) {
19
+ return { ...node, x: 100 + index * 10, y: 100 + index * 10 };
20
+ }
21
+ return node;
22
+ });
23
+ return helper.load(clampNode, normalizedFlow, credentials || {});
24
+ }
25
+
26
+ describe('MinMaxLimiterUltimate node', function () {
27
+ this.timeout(5000);
28
+
29
+ before(function (done) {
30
+ helper.startServer(done);
31
+ });
32
+
33
+ after(function (done) {
34
+ helper.stopServer(done);
35
+ });
36
+
37
+ afterEach(function () {
38
+ return helper.unload();
39
+ });
40
+
41
+ function baseFlow(extra) {
42
+ const flowId = 'clamp1';
43
+ return [
44
+ { id: flowId, type: 'tab', label: 'clamp1' },
45
+ Object.assign(
46
+ {
47
+ id: 'clamp',
48
+ type: 'MinMaxLimiterUltimate',
49
+ z: flowId,
50
+ min: 10,
51
+ max: 90,
52
+ payloadPropName: 'payload',
53
+ wires: [['out']],
54
+ },
55
+ extra || {}
56
+ ),
57
+ { id: 'in', type: 'helper', z: flowId, wires: [['clamp']] },
58
+ { id: 'out', type: 'helper', z: flowId },
59
+ ];
60
+ }
61
+
62
+ it('clamps below min, above max and passes through in range', function (done) {
63
+ loadClamp(baseFlow()).then(() => {
64
+ const clamp = helper.getNode('clamp');
65
+ const out = helper.getNode('out');
66
+ const results = [];
67
+
68
+ out.on('input', (msg) => {
69
+ results.push(msg.payload);
70
+ if (results.length === 3) {
71
+ try {
72
+ expect(results).to.deep.equal([10, 50, 90]);
73
+ done();
74
+ } catch (error) {
75
+ done(error);
76
+ }
77
+ }
78
+ });
79
+
80
+ clamp.receive({ payload: 0 });
81
+ clamp.receive({ payload: 50 });
82
+ clamp.receive({ payload: 100 });
83
+ }).catch(done);
84
+ });
85
+
86
+ it('exposes msg.clamp diagnostics', function (done) {
87
+ loadClamp(baseFlow()).then(() => {
88
+ const clamp = helper.getNode('clamp');
89
+ const out = helper.getNode('out');
90
+
91
+ out.on('input', (msg) => {
92
+ try {
93
+ expect(msg.clamp).to.deep.equal({
94
+ clamped: true,
95
+ input: 5,
96
+ output: 10,
97
+ min: 10,
98
+ max: 90,
99
+ });
100
+ done();
101
+ } catch (error) {
102
+ done(error);
103
+ }
104
+ });
105
+
106
+ clamp.receive({ payload: 5 });
107
+ }).catch(done);
108
+ });
109
+
110
+ it('updates limits at runtime via control topic', function (done) {
111
+ loadClamp(baseFlow({ controlTopic: 'clamp' })).then(() => {
112
+ const clamp = helper.getNode('clamp');
113
+ const out = helper.getNode('out');
114
+
115
+ out.on('input', (msg) => {
116
+ try {
117
+ expect(msg.payload).to.equal(20);
118
+ done();
119
+ } catch (error) {
120
+ done(error);
121
+ }
122
+ });
123
+
124
+ clamp.receive({ topic: 'clamp', min: 20, max: 80 });
125
+ clamp.receive({ payload: 5 });
126
+ }).catch(done);
127
+ });
128
+ });