node-red-contrib-boolean-logic-ultimate 1.2.8 → 1.2.11

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.
@@ -0,0 +1,197 @@
1
+ 'use strict';
2
+
3
+ module.exports = function (RED) {
4
+ const helpers = require('./lib/node-helpers.js');
5
+
6
+ function HysteresisUltimate(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 || 'hysteresis';
14
+ const payloadPropName = config.payloadPropName || 'payload';
15
+ const mode = (config.mode || 'high').toLowerCase(); // high | low
16
+ const emitOnlyOnChange = config.emitOnlyOnChange !== false;
17
+
18
+ let onThreshold = Number(config.onThreshold);
19
+ let offThreshold = Number(config.offThreshold);
20
+ let state = Boolean(config.initialState);
21
+
22
+ if (!Number.isFinite(onThreshold)) onThreshold = 70;
23
+ if (!Number.isFinite(offThreshold)) offThreshold = 65;
24
+
25
+ function toNumber(value) {
26
+ if (typeof value === 'number' && Number.isFinite(value)) {
27
+ return value;
28
+ }
29
+ if (typeof value === 'boolean') {
30
+ return value ? 1 : 0;
31
+ }
32
+ if (typeof value === 'string') {
33
+ const v = Number(value.trim());
34
+ return Number.isFinite(v) ? v : undefined;
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ function evaluateTyped(typedValue, typedType, baseMsg, fallback) {
40
+ try {
41
+ return REDUtil.evaluateNodeProperty(typedValue, typedType || 'bool', node, baseMsg);
42
+ } catch (error) {
43
+ return fallback;
44
+ }
45
+ }
46
+
47
+ function updateStatus(lastValue) {
48
+ const direction = mode === 'low' ? 'LOW' : 'HIGH';
49
+ const valueText = lastValue === undefined ? '-' : Number(lastValue).toFixed(2);
50
+ setNodeStatus({
51
+ fill: state ? 'green' : 'grey',
52
+ shape: state ? 'dot' : 'ring',
53
+ text: `${direction} ${valueText} on:${onThreshold} off:${offThreshold}`,
54
+ });
55
+ }
56
+
57
+ function evaluateState(value) {
58
+ let nextState = state;
59
+ if (mode === 'low') {
60
+ if (value <= onThreshold) {
61
+ nextState = true;
62
+ } else if (value >= offThreshold) {
63
+ nextState = false;
64
+ }
65
+ } else {
66
+ if (value >= onThreshold) {
67
+ nextState = true;
68
+ } else if (value <= offThreshold) {
69
+ nextState = false;
70
+ }
71
+ }
72
+ return nextState;
73
+ }
74
+
75
+ function emitDiagnostics(baseMsg, payload) {
76
+ const msg = baseMsg ? REDUtil.cloneMessage(baseMsg) : {};
77
+ msg.topic = `${controlTopic}/event`;
78
+ msg.payload = payload;
79
+ node.send([null, msg]);
80
+ }
81
+
82
+ function emitState(baseMsg, changed, inputValue) {
83
+ if (!changed && emitOnlyOnChange) {
84
+ updateStatus(inputValue);
85
+ return;
86
+ }
87
+ const msg = baseMsg ? REDUtil.cloneMessage(baseMsg) : {};
88
+ msg.payload = state
89
+ ? evaluateTyped(config.onPayload, config.onPayloadType, baseMsg, true)
90
+ : evaluateTyped(config.offPayload, config.offPayloadType, baseMsg, false);
91
+ msg.hysteresis = {
92
+ state,
93
+ changed,
94
+ mode,
95
+ value: inputValue,
96
+ onThreshold,
97
+ offThreshold,
98
+ };
99
+ msg.event = changed ? 'state_changed' : 'state_confirmed';
100
+ node.send([msg, null]);
101
+ updateStatus(inputValue);
102
+ }
103
+
104
+ function handleControl(msg) {
105
+ let consumed = false;
106
+
107
+ if (msg.reset === true) {
108
+ state = Boolean(config.initialState);
109
+ emitDiagnostics(msg, {
110
+ event: 'reset',
111
+ state,
112
+ onThreshold,
113
+ offThreshold,
114
+ mode,
115
+ });
116
+ consumed = true;
117
+ }
118
+
119
+ if (Object.prototype.hasOwnProperty.call(msg, 'onThreshold')) {
120
+ const next = Number(msg.onThreshold);
121
+ if (Number.isFinite(next)) {
122
+ onThreshold = next;
123
+ consumed = true;
124
+ }
125
+ }
126
+
127
+ if (Object.prototype.hasOwnProperty.call(msg, 'offThreshold')) {
128
+ const next = Number(msg.offThreshold);
129
+ if (Number.isFinite(next)) {
130
+ offThreshold = next;
131
+ consumed = true;
132
+ }
133
+ }
134
+
135
+ if (Object.prototype.hasOwnProperty.call(msg, 'state')) {
136
+ state = Boolean(msg.state);
137
+ emitState(msg, true, undefined);
138
+ consumed = true;
139
+ }
140
+
141
+ if (msg.status === true) {
142
+ emitDiagnostics(msg, {
143
+ event: 'status',
144
+ state,
145
+ mode,
146
+ onThreshold,
147
+ offThreshold,
148
+ });
149
+ consumed = true;
150
+ }
151
+
152
+ if (consumed) {
153
+ updateStatus();
154
+ }
155
+
156
+ return consumed;
157
+ }
158
+
159
+ node.on('input', (msg) => {
160
+ if (msg.topic === controlTopic && handleControl(msg)) {
161
+ return;
162
+ }
163
+
164
+ const resolved = helpers.resolveInput(msg, payloadPropName, config.translatorConfig, RED);
165
+ const inputValue = toNumber(resolved.value);
166
+
167
+ if (inputValue === undefined) {
168
+ emitDiagnostics(msg, {
169
+ event: 'invalid_input',
170
+ value: resolved.value,
171
+ property: payloadPropName,
172
+ });
173
+ return;
174
+ }
175
+
176
+ const nextState = evaluateState(inputValue);
177
+ const changed = nextState !== state;
178
+ state = nextState;
179
+ emitState(msg, changed, inputValue);
180
+
181
+ if (changed) {
182
+ emitDiagnostics(msg, {
183
+ event: 'state_changed',
184
+ state,
185
+ value: inputValue,
186
+ mode,
187
+ onThreshold,
188
+ offThreshold,
189
+ });
190
+ }
191
+ });
192
+
193
+ updateStatus();
194
+ }
195
+
196
+ RED.nodes.registerType('HysteresisUltimate', HysteresisUltimate);
197
+ };
@@ -104,9 +104,9 @@
104
104
  <div class="form-row rate-config-section rate-config-debounce">
105
105
  <label for="node-input-emitOn"><i class="fa fa-exchange"></i> Emit</label>
106
106
  <select id="node-input-emitOn">
107
- <option value="leading">Leading edge</option>
108
- <option value="trailing">Trailing edge</option>
109
- <option value="both">Both</option>
107
+ <option value="leading">Send first message immediately</option>
108
+ <option value="trailing">Send only the last message after the pause</option>
109
+ <option value="both">Send first immediately and last after the pause</option>
110
110
  </select>
111
111
  </div>
112
112
 
@@ -116,7 +116,7 @@
116
116
  </div>
117
117
 
118
118
  <div class="form-row rate-config-section rate-config-throttle">
119
- <label for="node-input-trailing"><i class="fa fa-arrow-circle-right"></i> Emit trailing</label>
119
+ <label for="node-input-trailing"><i class="fa fa-arrow-circle-right"></i> Also send the last queued message</label>
120
120
  <input type="checkbox" id="node-input-trailing" style="width:auto; margin-top:7px;">
121
121
  </div>
122
122
 
@@ -144,8 +144,8 @@ The purpose of this node is to moderate the frequency of incoming messages.
144
144
 
145
145
  **Modes**
146
146
 
147
- - **Debounce** – waits for the line to be quiet before forwarding the final message. With *Leading* it emits the first message immediately and blocks the following ones until the delay ends; *Trailing* sends only the last message; *Both* combines both behaviours.
148
- - **Throttle** – enforces a minimum interval between consecutive messages. When *Emit trailing* is enabled the most recent message is forwarded once the interval has elapsed.
147
+ - **Debounce** – waits for the line to be quiet before forwarding a message. You can choose to send the first message immediately, only the last message after the pause, or both.
148
+ - **Throttle** – enforces a minimum interval between consecutive messages. When *Also send the last queued message* is enabled the most recent message is forwarded once the interval has elapsed.
149
149
  - **Window** – limits the number of messages in a moving time window. Extra messages can be dropped or queued to play once a slot becomes available.
150
150
 
151
151
  **Outputs**
@@ -161,4 +161,4 @@ The purpose of this node is to moderate the frequency of incoming messages.
161
161
  - `msg.interval`, `msg.wait`, `msg.windowSize`, `msg.maxInWindow` – adjust the related thresholds.
162
162
 
163
163
  The **With Input** field selects which message property to evaluate (default `msg.payload`). When configured, it can be translated through the **translator-config** node for Home Assistant compatibility.
164
- </script>
164
+ </script>
@@ -0,0 +1,168 @@
1
+ [
2
+ {
3
+ "id": "db_tab_1",
4
+ "type": "tab",
5
+ "label": "DebouncerUltimate - burst filter",
6
+ "disabled": false,
7
+ "info": "Esempio: filtra burst ravvicinati con il nodo DebouncerUltimate."
8
+ },
9
+ {
10
+ "id": "db_cmt_1",
11
+ "type": "comment",
12
+ "z": "db_tab_1",
13
+ "name": "Burst → Debouncer → Forward",
14
+ "info": "Clicca BURST per generare messaggi ravvicinati. Cambia la modalità con LEADING/TRAILING/BOTH oppure usa FLUSH/RESET via topic \"debouncer\".",
15
+ "x": 300,
16
+ "y": 60,
17
+ "wires": []
18
+ },
19
+ {
20
+ "id": "db_inj_burst",
21
+ "type": "inject",
22
+ "z": "db_tab_1",
23
+ "name": "BURST (10 msgs)",
24
+ "props": [
25
+ { "p": "count", "v": "10", "vt": "num" },
26
+ { "p": "interval", "v": "50", "vt": "num" }
27
+ ],
28
+ "repeat": "",
29
+ "crontab": "",
30
+ "once": false,
31
+ "onceDelay": 0.1,
32
+ "x": 150,
33
+ "y": 140,
34
+ "wires": [["db_fn_burst"]]
35
+ },
36
+ {
37
+ "id": "db_fn_burst",
38
+ "type": "function",
39
+ "z": "db_tab_1",
40
+ "name": "Burst generator",
41
+ "func": "const count = Number(msg.count || 10);\nconst interval = Number(msg.interval || 50);\nlet i = 0;\n\nconst sendOne = () => {\n node.send({ topic: 'data', payload: i, index: i, ts: Date.now() });\n i += 1;\n if (i < count) {\n setTimeout(sendOne, interval);\n }\n};\n\nsendOne();\nreturn null;",
42
+ "outputs": 1,
43
+ "noerr": 0,
44
+ "initialize": "",
45
+ "finalize": "",
46
+ "libs": [],
47
+ "x": 360,
48
+ "y": 140,
49
+ "wires": [["db_node_1"]]
50
+ },
51
+ {
52
+ "id": "db_inj_leading",
53
+ "type": "inject",
54
+ "z": "db_tab_1",
55
+ "name": "LEADING (400ms)",
56
+ "props": [
57
+ { "p": "topic", "v": "debouncer", "vt": "str" },
58
+ { "p": "emitOn", "v": "leading", "vt": "str" },
59
+ { "p": "wait", "v": "400", "vt": "num" }
60
+ ],
61
+ "repeat": "",
62
+ "crontab": "",
63
+ "once": false,
64
+ "onceDelay": 0.1,
65
+ "x": 160,
66
+ "y": 220,
67
+ "wires": [["db_node_1"]]
68
+ },
69
+ {
70
+ "id": "db_inj_trailing",
71
+ "type": "inject",
72
+ "z": "db_tab_1",
73
+ "name": "TRAILING (400ms)",
74
+ "props": [
75
+ { "p": "topic", "v": "debouncer", "vt": "str" },
76
+ { "p": "emitOn", "v": "trailing", "vt": "str" },
77
+ { "p": "wait", "v": "400", "vt": "num" }
78
+ ],
79
+ "repeat": "",
80
+ "crontab": "",
81
+ "once": false,
82
+ "onceDelay": 0.1,
83
+ "x": 170,
84
+ "y": 260,
85
+ "wires": [["db_node_1"]]
86
+ },
87
+ {
88
+ "id": "db_inj_both",
89
+ "type": "inject",
90
+ "z": "db_tab_1",
91
+ "name": "BOTH (400ms)",
92
+ "props": [
93
+ { "p": "topic", "v": "debouncer", "vt": "str" },
94
+ { "p": "emitOn", "v": "both", "vt": "str" },
95
+ { "p": "wait", "v": "400", "vt": "num" }
96
+ ],
97
+ "repeat": "",
98
+ "crontab": "",
99
+ "once": false,
100
+ "onceDelay": 0.1,
101
+ "x": 150,
102
+ "y": 300,
103
+ "wires": [["db_node_1"]]
104
+ },
105
+ {
106
+ "id": "db_inj_flush",
107
+ "type": "inject",
108
+ "z": "db_tab_1",
109
+ "name": "FLUSH",
110
+ "props": [
111
+ { "p": "topic", "v": "debouncer", "vt": "str" },
112
+ { "p": "flush", "v": "true", "vt": "bool" }
113
+ ],
114
+ "repeat": "",
115
+ "crontab": "",
116
+ "once": false,
117
+ "onceDelay": 0.1,
118
+ "x": 120,
119
+ "y": 340,
120
+ "wires": [["db_node_1"]]
121
+ },
122
+ {
123
+ "id": "db_inj_reset",
124
+ "type": "inject",
125
+ "z": "db_tab_1",
126
+ "name": "RESET",
127
+ "props": [
128
+ { "p": "topic", "v": "debouncer", "vt": "str" },
129
+ { "p": "reset", "v": "true", "vt": "bool" }
130
+ ],
131
+ "repeat": "",
132
+ "crontab": "",
133
+ "once": false,
134
+ "onceDelay": 0.1,
135
+ "x": 120,
136
+ "y": 380,
137
+ "wires": [["db_node_1"]]
138
+ },
139
+ {
140
+ "id": "db_node_1",
141
+ "type": "DebouncerUltimate",
142
+ "z": "db_tab_1",
143
+ "name": "Debouncer",
144
+ "wait": 400,
145
+ "emitOn": "trailing",
146
+ "controlTopic": "debouncer",
147
+ "x": 590,
148
+ "y": 240,
149
+ "wires": [["db_dbg_fwd"]]
150
+ },
151
+ {
152
+ "id": "db_dbg_fwd",
153
+ "type": "debug",
154
+ "z": "db_tab_1",
155
+ "name": "Forward",
156
+ "active": true,
157
+ "tosidebar": true,
158
+ "console": false,
159
+ "tostatus": false,
160
+ "complete": "true",
161
+ "targetType": "full",
162
+ "statusVal": "",
163
+ "statusType": "auto",
164
+ "x": 780,
165
+ "y": 220,
166
+ "wires": []
167
+ }
168
+ ]
@@ -0,0 +1,109 @@
1
+ [
2
+ {
3
+ "id": "hys-flow",
4
+ "type": "tab",
5
+ "label": "HysteresisUltimate"
6
+ },
7
+ {
8
+ "id": "hys-node",
9
+ "type": "HysteresisUltimate",
10
+ "z": "hys-flow",
11
+ "name": "Bathroom Humidity",
12
+ "controlTopic": "hysteresis",
13
+ "payloadPropName": "payload",
14
+ "translatorConfig": "",
15
+ "mode": "high",
16
+ "onThreshold": 70,
17
+ "offThreshold": 62,
18
+ "initialState": false,
19
+ "emitOnlyOnChange": true,
20
+ "onPayload": "on",
21
+ "onPayloadType": "str",
22
+ "offPayload": "off",
23
+ "offPayloadType": "str",
24
+ "x": 520,
25
+ "y": 200,
26
+ "wires": [["hys-out"], ["hys-diag"]]
27
+ },
28
+ {
29
+ "id": "hys-in-1",
30
+ "type": "inject",
31
+ "z": "hys-flow",
32
+ "name": "Humidity 68",
33
+ "props": [
34
+ { "p": "payload", "v": "68", "vt": "num" },
35
+ { "p": "topic", "v": "sensor/bathroom/humidity", "vt": "str" }
36
+ ],
37
+ "repeat": "",
38
+ "crontab": "",
39
+ "once": false,
40
+ "onceDelay": 0.1,
41
+ "x": 260,
42
+ "y": 160,
43
+ "wires": [["hys-node"]]
44
+ },
45
+ {
46
+ "id": "hys-in-2",
47
+ "type": "inject",
48
+ "z": "hys-flow",
49
+ "name": "Humidity 72",
50
+ "props": [
51
+ { "p": "payload", "v": "72", "vt": "num" },
52
+ { "p": "topic", "v": "sensor/bathroom/humidity", "vt": "str" }
53
+ ],
54
+ "repeat": "",
55
+ "crontab": "",
56
+ "once": false,
57
+ "onceDelay": 0.1,
58
+ "x": 260,
59
+ "y": 200,
60
+ "wires": [["hys-node"]]
61
+ },
62
+ {
63
+ "id": "hys-in-3",
64
+ "type": "inject",
65
+ "z": "hys-flow",
66
+ "name": "Humidity 60",
67
+ "props": [
68
+ { "p": "payload", "v": "60", "vt": "num" },
69
+ { "p": "topic", "v": "sensor/bathroom/humidity", "vt": "str" }
70
+ ],
71
+ "repeat": "",
72
+ "crontab": "",
73
+ "once": false,
74
+ "onceDelay": 0.1,
75
+ "x": 260,
76
+ "y": 240,
77
+ "wires": [["hys-node"]]
78
+ },
79
+ {
80
+ "id": "hys-out",
81
+ "type": "debug",
82
+ "z": "hys-flow",
83
+ "name": "HA Fan Command",
84
+ "active": true,
85
+ "tosidebar": true,
86
+ "console": false,
87
+ "tostatus": false,
88
+ "complete": "true",
89
+ "targetType": "full",
90
+ "x": 780,
91
+ "y": 180,
92
+ "wires": []
93
+ },
94
+ {
95
+ "id": "hys-diag",
96
+ "type": "debug",
97
+ "z": "hys-flow",
98
+ "name": "Diagnostics",
99
+ "active": true,
100
+ "tosidebar": true,
101
+ "console": false,
102
+ "tostatus": false,
103
+ "complete": "payload",
104
+ "targetType": "msg",
105
+ "x": 760,
106
+ "y": 240,
107
+ "wires": []
108
+ }
109
+ ]
@@ -8,11 +8,12 @@ How to import:
8
8
 
9
9
  ## Available example flows
10
10
 
11
- - `AlarmSystemUltimate.json` — Alarm System Ultimate (BETA)
12
11
  - `BlinkerUltimate.json` — BlinkerUltimate
13
12
  - `BooleanLogicUltimate.json` — BooleanLogicUltimate
14
13
  - `Comparator.json` — Comparator
14
+ - `DebouncerUltimate.json` — DebouncerUltimate
15
15
  - `FilterUltimate.json` — FilterUltimate
16
+ - `HysteresisUltimate.json` — HysteresisUltimate
16
17
  - `ImpulseUltimate.json` — ImpulseUltimate
17
18
  - `InjectUltimate.json` — InjectUltimate
18
19
  - `InterruptFlowUltimate.json` — InterruptFlowUltimate
@@ -27,4 +28,3 @@ How to import:
27
28
  - `SumUltimate.json` — SumUltimate
28
29
  - `toggleUltimate.json` — toggleUltimate
29
30
  - `translator-config.json` — translator-config
30
-
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "node-red-contrib-boolean-logic-ultimate",
3
- "version": "1.2.8",
4
- "description": "A set of Node-RED enhanced boolean logic and utility nodes, flow interruption, blinker, invert, filter, toggle etc.., with persistent values after reboot. Compatible also with Homeassistant values.",
3
+ "version": "1.2.11",
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": {
7
7
  "fs": "0.0.1-security",
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "node-red": {
30
30
  "nodes": {
31
+ "DebouncerUltimate": "boolean-logic-ultimate/DebouncerUltimate.js",
31
32
  "BooleanLogicUltimate": "boolean-logic-ultimate/BooleanLogicUltimate.js",
32
33
  "InvertUltimate": "boolean-logic-ultimate/InvertUltimate.js",
33
34
  "FilterUltimate": "boolean-logic-ultimate/FilterUltimate.js",
@@ -45,10 +46,11 @@
45
46
  "RateLimiterUltimate": "boolean-logic-ultimate/RateLimiterUltimate.js",
46
47
  "PresenceSimulatorUltimate": "boolean-logic-ultimate/PresenceSimulatorUltimate.js",
47
48
  "StaircaseLightUltimate": "boolean-logic-ultimate/StaircaseLightUltimate.js",
49
+ "HysteresisUltimate": "boolean-logic-ultimate/HysteresisUltimate.js",
48
50
  "translator-config": "boolean-logic-ultimate/translator-config.js"
49
51
  }
50
52
  },
51
53
  "scripts": {
52
54
  "test": "mocha test/**/*.spec.js"
53
55
  }
54
- }
56
+ }