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

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.9</b> April 2026<br/>
9
+ - Removed the following newly introduced HA nodes because considered too complex: ForDurationUltimate, WatchdogUltimate, PriorityMuxUltimate, GroupStateUltimate.<br/>
10
+ - Removed related examples, tests and documentation sections.<br/>
11
+ - Kept HysteresisUltimate and aligned package registration/examples docs accordingly.<br/>
12
+ </p>
13
+
7
14
  <p>
8
15
  <b>Version 1.2.8</b> January 2026<br/>
9
16
  - Added youtube videos for the missing nodes and the relative links to the UI<br/>
package/README.md CHANGED
@@ -720,63 +720,6 @@ Each event in the sequence outputs a message configured in the JSON line. When r
720
720
 
721
721
  <br/>
722
722
 
723
- # ALARM SYSTEM ULTIMATE (BETA)
724
-
725
- This node implements an alarm control panel with multi-mode arming, zones, entry/exit delays, bypass, tamper/fire 24h zones, siren control, status and event log.
726
-
727
- Example flow: [`examples/AlarmSystemUltimate.json`](examples/AlarmSystemUltimate.json)
728
-
729
- ### NODE CONFIGURATION
730
-
731
- | Property | Description |
732
- | --------------------------- | ------------------------------------------------------------------------------------------------------------ |
733
- | Control topic | Topic that receives runtime commands such as arm/disarm/status/bypass. |
734
- | With Input | Message property evaluated as sensor value (default `payload`). |
735
- | Persist state | Persists arming mode, bypass list and last log entries across restarts. |
736
- | Require code for arm/disarm | Enables PIN checks using `msg.code` (or `msg.pin`). |
737
- | Exit/Entry delay (s) | Global exit/entry delays (each zone can override entry delay). |
738
- | Siren topic | Topic used on output 2 to turn the siren on/off. |
739
- | Siren payloads | Values emitted on output 2 for siren on/off (typed). |
740
- | Siren duration (s) | Auto stop duration (0 = latch until disarm). |
741
- | Emit restore events | Emits `zone_restore` when a zone returns to false. |
742
- | Event log size | Max stored log entries in node context. |
743
- | Zones | One JSON object per line (legacy) or a JSON array (formatted). Use **Format** in the editor to pretty-print. |
744
-
745
- Esempio JSON di una zona:
746
-
747
- ```json
748
- {
749
- "id": "front_door",
750
- "name": "Front Door",
751
- "topic": "house/door/front",
752
- "type": "perimeter",
753
- "modes": ["away", "night"],
754
- "entry": true,
755
- "entryDelaySeconds": 30,
756
- "bypassable": true,
757
- "chime": true
758
- }
759
- ```
760
-
761
- ### OUTPUTS
762
-
763
- - Output 1 (Events): `msg.topic = <controlTopic>/event`, with `msg.event` and `msg.payload` (state + details).
764
- - Output 2 (Siren): emits siren on/off commands on `sirenTopic` with the configured payloads.
765
-
766
- ### CONTROL MESSAGES (`msg.topic === controlTopic`)
767
-
768
- - Arm: `msg.command = 'arm_away'|'arm_home'|'arm_night'` or `msg.arm = 'away'|'home'|'night'`
769
- - Disarm: `msg.command = 'disarm'` or `msg.disarm = true`
770
- - Status: `msg.command = 'status'` or `msg.status = true`
771
- - Bypass: `msg.command = 'bypass'|'unbypass'` with `msg.zone = '<zone id>'`
772
- - Panic: `msg.command = 'panic'` or `msg.command = 'panic_silent'`
773
- - Siren: `msg.command = 'siren_on'|'siren_off'`
774
- - Reset: `msg.command = 'reset'` or `msg.reset = true`
775
-
776
- When codes are enabled, pass `msg.code` (or `msg.pin`). If `duressCode` matches, the node raises a silent duress alarm event.
777
-
778
- <br/>
779
-
780
723
  # STAIRCASE LIGHT ULTIMATE
781
724
 
782
725
  The purpose of this node is to control staircase lighting with a timer, pre-off warning and optional extension on every trigger.
@@ -807,6 +750,36 @@ Output 1 delivers the ON/OFF command. Output 2 delivers the warning and includes
807
750
 
808
751
  <br/>
809
752
 
753
+ # HYSTERESIS ULTIMATE
754
+
755
+ Adds hysteresis to numeric sensor values to avoid rapid ON/OFF bouncing around thresholds.
756
+
757
+ Example flow: [`examples/HysteresisUltimate.json`](examples/HysteresisUltimate.json)
758
+
759
+ ### NODE CONFIGURATION
760
+
761
+ | Property | Description |
762
+ | ------------------- | ------------------------------------------------------------------------------------------------------------ |
763
+ | Control topic | Topic that receives runtime commands such as threshold updates and reset. |
764
+ | Mode | `high` = ON above threshold, OFF below. `low` = ON below threshold, OFF above. |
765
+ | ON/OFF threshold | Hysteresis limits. |
766
+ | Initial state | Startup output state. |
767
+ | Emit only on change | If enabled, output 1 emits only on state transitions. |
768
+ | With Input | Message property evaluated as numeric value (default `payload`). |
769
+ | Translator | Optional translator-config. |
770
+ | On/Off payload | Typed payloads sent on output 1. |
771
+
772
+ ### CONTROL MESSAGES (`msg.topic === controlTopic`)
773
+
774
+ - `msg.onThreshold`, `msg.offThreshold` &rarr; update thresholds at runtime.
775
+ - `msg.state = true|false` &rarr; force state.
776
+ - `msg.reset = true` &rarr; restore initial state.
777
+ - `msg.status = true` &rarr; emit current state/thresholds on output 2.
778
+
779
+ Output 1 emits the command payload (`onPayload`/`offPayload`). Output 2 emits diagnostic events.
780
+
781
+ <br/>
782
+
810
783
  [license-image]: https://img.shields.io/badge/license-MIT-blue.svg
811
784
  [license-url]: https://github.com/Supergiovane/node-red-contrib-boolean-logic-ultimate/master/LICENSE
812
785
  [npm-url]: https://npmjs.org/package/node-red-contrib-boolean-logic-ultimate
@@ -0,0 +1,137 @@
1
+ <script type="text/javascript">
2
+ RED.nodes.registerType('HysteresisUltimate', {
3
+ category: 'Boolean Logic Ultimate',
4
+ color: '#ff8080',
5
+ defaults: {
6
+ name: { value: '' },
7
+ controlTopic: { value: 'hysteresis' },
8
+ payloadPropName: { value: 'payload', required: false },
9
+ translatorConfig: { type: 'translator-config', required: false },
10
+ mode: { value: 'high' },
11
+ onThreshold: { value: 70, validate: RED.validators.number() },
12
+ offThreshold: { value: 65, validate: RED.validators.number() },
13
+ initialState: { value: false },
14
+ emitOnlyOnChange: { value: true },
15
+ onPayload: { value: true },
16
+ onPayloadType: { value: 'bool' },
17
+ offPayload: { value: false },
18
+ offPayloadType: { value: 'bool' }
19
+ },
20
+ inputs: 1,
21
+ outputs: 2,
22
+ outputLabels: ['State output', 'Diagnostics'],
23
+ icon: 'font-awesome/fa-sliders',
24
+ label: function () {
25
+ return this.name || 'Hysteresis';
26
+ },
27
+ paletteLabel: function () {
28
+ return 'Hysteresis';
29
+ },
30
+ oneditprepare: function () {
31
+ const payloadField = $('#node-input-payloadPropName');
32
+ if (payloadField.val() === '') payloadField.val('payload');
33
+ payloadField.typedInput({ default: 'msg', types: ['msg'] });
34
+
35
+ $('#node-input-onPayload').typedInput({
36
+ default: this.onPayloadType || 'bool',
37
+ types: ['str', 'num', 'bool', 'json']
38
+ });
39
+ $('#node-input-onPayload').typedInput('type', this.onPayloadType || 'bool');
40
+
41
+ $('#node-input-offPayload').typedInput({
42
+ default: this.offPayloadType || 'bool',
43
+ types: ['str', 'num', 'bool', 'json']
44
+ });
45
+ $('#node-input-offPayload').typedInput('type', this.offPayloadType || 'bool');
46
+ },
47
+ oneditsave: function () {
48
+ this.onPayloadType = $('#node-input-onPayload').typedInput('type');
49
+ this.offPayloadType = $('#node-input-offPayload').typedInput('type');
50
+ }
51
+ });
52
+ </script>
53
+
54
+ <script type="text/html" data-template-name="HysteresisUltimate">
55
+ <div class="form-row">
56
+ <b>Hysteresis Ultimate</b>
57
+ &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>
58
+ </div>
59
+
60
+ <div class="form-row">
61
+ <label for="node-input-name"><i class="icon-tag"></i> Name</label>
62
+ <input type="text" id="node-input-name" placeholder="Name">
63
+ </div>
64
+
65
+ <div class="form-row">
66
+ <label for="node-input-controlTopic"><i class="fa fa-tag"></i> Control topic</label>
67
+ <input type="text" id="node-input-controlTopic">
68
+ </div>
69
+
70
+ <div class="form-row">
71
+ <label for="node-input-mode"><i class="fa fa-arrows-v"></i> Mode</label>
72
+ <select id="node-input-mode">
73
+ <option value="high">High threshold (ON above)</option>
74
+ <option value="low">Low threshold (ON below)</option>
75
+ </select>
76
+ </div>
77
+
78
+ <div class="form-row">
79
+ <label for="node-input-onThreshold"><i class="fa fa-long-arrow-up"></i> ON threshold</label>
80
+ <input type="number" id="node-input-onThreshold" step="any">
81
+ </div>
82
+
83
+ <div class="form-row">
84
+ <label for="node-input-offThreshold"><i class="fa fa-long-arrow-down"></i> OFF threshold</label>
85
+ <input type="number" id="node-input-offThreshold" step="any">
86
+ </div>
87
+
88
+ <div class="form-row">
89
+ <label for="node-input-initialState"><i class="fa fa-power-off"></i> Initial state ON</label>
90
+ <input type="checkbox" id="node-input-initialState" style="width:auto; margin-top:7px;">
91
+ </div>
92
+
93
+ <div class="form-row">
94
+ <label for="node-input-emitOnlyOnChange"><i class="fa fa-filter"></i> Emit only on change</label>
95
+ <input type="checkbox" id="node-input-emitOnlyOnChange" style="width:auto; margin-top:7px;">
96
+ </div>
97
+
98
+ <div class="form-row">
99
+ <label for="node-input-payloadPropName"><i class="fa fa-ellipsis-h"></i> With Input</label>
100
+ <input type="text" id="node-input-payloadPropName">
101
+ </div>
102
+
103
+ <div class="form-row">
104
+ <label for="node-input-translatorConfig"><i class="fa fa-language"></i> Translator</label>
105
+ <input type="text" id="node-input-translatorConfig">
106
+ </div>
107
+
108
+ <div class="form-row">
109
+ <label for="node-input-onPayload"><i class="fa fa-sign-in"></i> On payload</label>
110
+ <input type="text" id="node-input-onPayload">
111
+ </div>
112
+
113
+ <div class="form-row">
114
+ <label for="node-input-offPayload"><i class="fa fa-sign-out"></i> Off payload</label>
115
+ <input type="text" id="node-input-offPayload">
116
+ </div>
117
+ </script>
118
+
119
+ <script type="text/markdown" data-help-name="HysteresisUltimate">
120
+ <p>Adds hysteresis to numeric values to avoid rapid ON/OFF bouncing around a threshold.</p>
121
+
122
+ |Property|Description|
123
+ |--|--|
124
+ | Mode | `high`: ON above threshold, OFF below. `low`: ON below threshold, OFF above. |
125
+ | ON/OFF threshold | The two hysteresis limits. |
126
+ | Emit only on change | Emits output only when state changes. |
127
+ | With Input | Message property to evaluate (default `payload`). |
128
+ | Translator | Optional translator-config. |
129
+ | On/Off payload | Values sent on output 1. |
130
+
131
+ Control topic messages:
132
+
133
+ - `msg.onThreshold`, `msg.offThreshold` to update thresholds at runtime.
134
+ - `msg.state = true|false` to force state.
135
+ - `msg.reset = true` to restore initial state.
136
+ - `msg.status = true` to emit current status on output 2.
137
+ </script>
@@ -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
+ };
@@ -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,11 @@ 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
15
14
  - `FilterUltimate.json` — FilterUltimate
15
+ - `HysteresisUltimate.json` — HysteresisUltimate
16
16
  - `ImpulseUltimate.json` — ImpulseUltimate
17
17
  - `InjectUltimate.json` — InjectUltimate
18
18
  - `InterruptFlowUltimate.json` — InterruptFlowUltimate
@@ -27,4 +27,3 @@ How to import:
27
27
  - `SumUltimate.json` — SumUltimate
28
28
  - `toggleUltimate.json` — toggleUltimate
29
29
  - `translator-config.json` — translator-config
30
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-boolean-logic-ultimate",
3
- "version": "1.2.8",
3
+ "version": "1.2.9",
4
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.",
5
5
  "author": "Supergiovane (https://github.com/Supergiovane)",
6
6
  "dependencies": {
@@ -45,10 +45,11 @@
45
45
  "RateLimiterUltimate": "boolean-logic-ultimate/RateLimiterUltimate.js",
46
46
  "PresenceSimulatorUltimate": "boolean-logic-ultimate/PresenceSimulatorUltimate.js",
47
47
  "StaircaseLightUltimate": "boolean-logic-ultimate/StaircaseLightUltimate.js",
48
+ "HysteresisUltimate": "boolean-logic-ultimate/HysteresisUltimate.js",
48
49
  "translator-config": "boolean-logic-ultimate/translator-config.js"
49
50
  }
50
51
  },
51
52
  "scripts": {
52
53
  "test": "mocha test/**/*.spec.js"
53
54
  }
54
- }
55
+ }
@@ -0,0 +1,87 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const { helper } = require('./helpers');
5
+
6
+ const hysteresisNode = require('../boolean-logic-ultimate/HysteresisUltimate.js');
7
+
8
+ function loadHysteresis(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(hysteresisNode, normalizedFlow, credentials || {});
24
+ }
25
+
26
+ describe('HysteresisUltimate 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
+ it('emits on state transitions only', function (done) {
42
+ const flowId = 'hys1';
43
+ const flow = [
44
+ { id: flowId, type: 'tab', label: 'hys1' },
45
+ {
46
+ id: 'hys',
47
+ type: 'HysteresisUltimate',
48
+ z: flowId,
49
+ mode: 'high',
50
+ onThreshold: 70,
51
+ offThreshold: 60,
52
+ emitOnlyOnChange: true,
53
+ onPayload: true,
54
+ onPayloadType: 'bool',
55
+ offPayload: false,
56
+ offPayloadType: 'bool',
57
+ wires: [['out'], ['diag']],
58
+ },
59
+ { id: 'in', type: 'helper', z: flowId, wires: [['hys']] },
60
+ { id: 'out', type: 'helper', z: flowId },
61
+ { id: 'diag', type: 'helper', z: flowId },
62
+ ];
63
+
64
+ loadHysteresis(flow).then(() => {
65
+ const hys = helper.getNode('hys');
66
+ const out = helper.getNode('out');
67
+ const results = [];
68
+
69
+ out.on('input', (msg) => {
70
+ results.push(msg.payload);
71
+ if (results.length === 2) {
72
+ try {
73
+ expect(results).to.deep.equal([true, false]);
74
+ done();
75
+ } catch (error) {
76
+ done(error);
77
+ }
78
+ }
79
+ });
80
+
81
+ hys.receive({ topic: 'sensor', payload: 65 });
82
+ hys.receive({ topic: 'sensor', payload: 72 });
83
+ hys.receive({ topic: 'sensor', payload: 68 });
84
+ hys.receive({ topic: 'sensor', payload: 58 });
85
+ }).catch(done);
86
+ });
87
+ });
@@ -1,286 +0,0 @@
1
- [
2
- {
3
- "id": "al_tab_1",
4
- "type": "tab",
5
- "label": "AlarmSystemUltimate - demo base",
6
- "disabled": false,
7
- "info": "Esempio per il nodo ALARM (Alarm System Ultimate - BETA): armo/disarmo, bypass zone, trigger sensori, eventi e sirena."
8
- },
9
- {
10
- "id": "al_cmt_1",
11
- "type": "comment",
12
- "z": "al_tab_1",
13
- "name": "Topic di controllo = alarm | Topic sirena = siren",
14
- "info": "Comandi principali (topic=alarm): arm_away/arm_home/arm_night, disarm, status, bypass/unbypass (con msg.zone). I sensori entrano con msg.topic uguale a quello configurato nelle zone e payload booleano true/false.",
15
- "x": 260,
16
- "y": 60,
17
- "wires": []
18
- },
19
- {
20
- "id": "al_inj_arm_away",
21
- "type": "inject",
22
- "z": "al_tab_1",
23
- "name": "ARM AWAY (code=1234)",
24
- "props": [
25
- { "p": "topic", "v": "alarm", "vt": "str" },
26
- { "p": "command", "v": "arm_away", "vt": "str" },
27
- { "p": "code", "v": "1234", "vt": "str" }
28
- ],
29
- "repeat": "",
30
- "crontab": "",
31
- "once": false,
32
- "onceDelay": 0.1,
33
- "x": 160,
34
- "y": 140,
35
- "wires": [["al_alarm_1"]]
36
- },
37
- {
38
- "id": "al_inj_arm_home",
39
- "type": "inject",
40
- "z": "al_tab_1",
41
- "name": "ARM HOME (code=1234)",
42
- "props": [
43
- { "p": "topic", "v": "alarm", "vt": "str" },
44
- { "p": "command", "v": "arm_home", "vt": "str" },
45
- { "p": "code", "v": "1234", "vt": "str" }
46
- ],
47
- "repeat": "",
48
- "crontab": "",
49
- "once": false,
50
- "onceDelay": 0.1,
51
- "x": 160,
52
- "y": 180,
53
- "wires": [["al_alarm_1"]]
54
- },
55
- {
56
- "id": "al_inj_disarm",
57
- "type": "inject",
58
- "z": "al_tab_1",
59
- "name": "DISARM (code=1234)",
60
- "props": [
61
- { "p": "topic", "v": "alarm", "vt": "str" },
62
- { "p": "command", "v": "disarm", "vt": "str" },
63
- { "p": "code", "v": "1234", "vt": "str" }
64
- ],
65
- "repeat": "",
66
- "crontab": "",
67
- "once": false,
68
- "onceDelay": 0.1,
69
- "x": 160,
70
- "y": 220,
71
- "wires": [["al_alarm_1"]]
72
- },
73
- {
74
- "id": "al_inj_status",
75
- "type": "inject",
76
- "z": "al_tab_1",
77
- "name": "STATUS",
78
- "props": [
79
- { "p": "topic", "v": "alarm", "vt": "str" },
80
- { "p": "command", "v": "status", "vt": "str" }
81
- ],
82
- "repeat": "",
83
- "crontab": "",
84
- "once": false,
85
- "onceDelay": 0.1,
86
- "x": 110,
87
- "y": 260,
88
- "wires": [["al_alarm_1"]]
89
- },
90
- {
91
- "id": "al_inj_bypass_front",
92
- "type": "inject",
93
- "z": "al_tab_1",
94
- "name": "BYPASS front_door",
95
- "props": [
96
- { "p": "topic", "v": "alarm", "vt": "str" },
97
- { "p": "command", "v": "bypass", "vt": "str" },
98
- { "p": "zone", "v": "front_door", "vt": "str" }
99
- ],
100
- "repeat": "",
101
- "crontab": "",
102
- "once": false,
103
- "onceDelay": 0.1,
104
- "x": 140,
105
- "y": 320,
106
- "wires": [["al_alarm_1"]]
107
- },
108
- {
109
- "id": "al_inj_unbypass_front",
110
- "type": "inject",
111
- "z": "al_tab_1",
112
- "name": "UNBYPASS front_door",
113
- "props": [
114
- { "p": "topic", "v": "alarm", "vt": "str" },
115
- { "p": "command", "v": "unbypass", "vt": "str" },
116
- { "p": "zone", "v": "front_door", "vt": "str" }
117
- ],
118
- "repeat": "",
119
- "crontab": "",
120
- "once": false,
121
- "onceDelay": 0.1,
122
- "x": 150,
123
- "y": 360,
124
- "wires": [["al_alarm_1"]]
125
- },
126
- {
127
- "id": "al_cmt_2",
128
- "type": "comment",
129
- "z": "al_tab_1",
130
- "name": "Sensori (topic = zona) | payload true = trigger | payload false = restore",
131
- "info": "La zona front_door è con entry delay e chime quando disarmata. pir_living è attiva solo in away. smoke è fire 24/7 (sempre attiva).",
132
- "x": 320,
133
- "y": 420,
134
- "wires": []
135
- },
136
- {
137
- "id": "al_inj_front_open",
138
- "type": "inject",
139
- "z": "al_tab_1",
140
- "name": "Front door OPEN (true)",
141
- "props": [
142
- { "p": "topic", "v": "sensor/frontdoor", "vt": "str" },
143
- { "p": "payload", "v": "true", "vt": "bool" }
144
- ],
145
- "repeat": "",
146
- "crontab": "",
147
- "once": false,
148
- "onceDelay": 0.1,
149
- "x": 170,
150
- "y": 500,
151
- "wires": [["al_alarm_1"]]
152
- },
153
- {
154
- "id": "al_inj_front_close",
155
- "type": "inject",
156
- "z": "al_tab_1",
157
- "name": "Front door CLOSE (false)",
158
- "props": [
159
- { "p": "topic", "v": "sensor/frontdoor", "vt": "str" },
160
- { "p": "payload", "v": "false", "vt": "bool" }
161
- ],
162
- "repeat": "",
163
- "crontab": "",
164
- "once": false,
165
- "onceDelay": 0.1,
166
- "x": 180,
167
- "y": 540,
168
- "wires": [["al_alarm_1"]]
169
- },
170
- {
171
- "id": "al_inj_pir",
172
- "type": "inject",
173
- "z": "al_tab_1",
174
- "name": "PIR living (true)",
175
- "props": [
176
- { "p": "topic", "v": "sensor/living_pir", "vt": "str" },
177
- { "p": "payload", "v": "true", "vt": "bool" }
178
- ],
179
- "repeat": "",
180
- "crontab": "",
181
- "once": false,
182
- "onceDelay": 0.1,
183
- "x": 150,
184
- "y": 600,
185
- "wires": [["al_alarm_1"]]
186
- },
187
- {
188
- "id": "al_inj_smoke",
189
- "type": "inject",
190
- "z": "al_tab_1",
191
- "name": "SMOKE (true)",
192
- "props": [
193
- { "p": "topic", "v": "sensor/smoke", "vt": "str" },
194
- { "p": "payload", "v": "true", "vt": "bool" }
195
- ],
196
- "repeat": "",
197
- "crontab": "",
198
- "once": false,
199
- "onceDelay": 0.1,
200
- "x": 130,
201
- "y": 640,
202
- "wires": [["al_alarm_1"]]
203
- },
204
- {
205
- "id": "al_alarm_1",
206
- "type": "AlarmSystemUltimate",
207
- "z": "al_tab_1",
208
- "name": "ALARM",
209
- "controlTopic": "alarm",
210
- "payloadPropName": "payload",
211
- "translatorConfig": "",
212
- "persistState": true,
213
- "requireCodeForArm": true,
214
- "requireCodeForDisarm": true,
215
- "armCode": "1234",
216
- "duressCode": "9999",
217
- "blockArmOnViolations": true,
218
- "exitDelaySeconds": 10,
219
- "entryDelaySeconds": 5,
220
- "sirenDurationSeconds": 15,
221
- "sirenLatchUntilDisarm": false,
222
- "sirenTopic": "siren",
223
- "sirenOnPayload": true,
224
- "sirenOnPayloadType": "bool",
225
- "sirenOffPayload": false,
226
- "sirenOffPayloadType": "bool",
227
- "emitRestoreEvents": true,
228
- "maxLogEntries": 50,
229
- "zones": "{\"id\":\"front_door\",\"name\":\"Porta ingresso\",\"topic\":\"sensor/frontdoor\",\"type\":\"perimeter\",\"entry\":true,\"modes\":[\"away\",\"home\",\"night\"],\"bypassable\":true,\"chime\":true}\n{\"id\":\"pir_living\",\"name\":\"PIR soggiorno\",\"topic\":\"sensor/living_pir\",\"type\":\"motion\",\"modes\":[\"away\"],\"entry\":false,\"bypassable\":true,\"cooldownSeconds\":5}\n{\"id\":\"smoke\",\"name\":\"Fumo\",\"topic\":\"sensor/smoke\",\"type\":\"fire\",\"modes\":[],\"entry\":false,\"bypassable\":false}",
230
- "x": 450,
231
- "y": 280,
232
- "wires": [["al_dbg_evt"], ["al_dbg_siren"]]
233
- },
234
- {
235
- "id": "al_dbg_evt",
236
- "type": "debug",
237
- "z": "al_tab_1",
238
- "name": "Events (output 1)",
239
- "active": true,
240
- "tosidebar": true,
241
- "console": false,
242
- "tostatus": false,
243
- "complete": "true",
244
- "targetType": "full",
245
- "statusVal": "",
246
- "statusType": "auto",
247
- "x": 680,
248
- "y": 260,
249
- "wires": []
250
- },
251
- {
252
- "id": "al_dbg_siren",
253
- "type": "debug",
254
- "z": "al_tab_1",
255
- "name": "Siren (output 2)",
256
- "active": true,
257
- "tosidebar": true,
258
- "console": false,
259
- "tostatus": false,
260
- "complete": "true",
261
- "targetType": "full",
262
- "statusVal": "",
263
- "statusType": "auto",
264
- "x": 680,
265
- "y": 300,
266
- "wires": []
267
- },
268
- {
269
- "id": "al_inj_disarm_duress",
270
- "type": "inject",
271
- "z": "al_tab_1",
272
- "name": "DISARM (DURESS 9999)",
273
- "props": [
274
- { "p": "topic", "v": "alarm", "vt": "str" },
275
- { "p": "command", "v": "disarm", "vt": "str" },
276
- { "p": "code", "v": "9999", "vt": "str" }
277
- ],
278
- "repeat": "",
279
- "crontab": "",
280
- "once": false,
281
- "onceDelay": 0.1,
282
- "x": 160,
283
- "y": 400,
284
- "wires": [["al_alarm_1"]]
285
- }
286
- ]