node-red-contrib-timer-events 0.1.0

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,444 @@
1
+ <!--
2
+ timer-events
3
+ Derived from stoptimer-varidelay-plus.
4
+ Modifications copyright (C) 2025 mchristegh
5
+ Modifications copyright (C) 2020 hamsando
6
+ Copyright jbardi
7
+
8
+ Licensed under the Apache License, Version 2.0 (the "License");
9
+ you may not use this file except in compliance with the License.
10
+ You may obtain a copy of the License at
11
+
12
+ http://www.apache.org/licenses/LICENSE-2.0
13
+
14
+ Unless required by applicable law or agreed to in writing, software
15
+ distributed under the License is distributed on an "AS IS" BASIS,
16
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ See the License for the specific language governing permissions and
18
+ limitations under the License.
19
+ -->
20
+
21
+ <script type="text/javascript">
22
+ RED.nodes.registerType('timer-events',{
23
+ category: 'function',
24
+ color:"#869869",
25
+ defaults: {
26
+ duration: {value:"5",required:true,validate:RED.validators.typedInput("durationType")},
27
+ durationType: {value:'num'},
28
+ units: {value:"Second"},
29
+ name: {value:""},
30
+ reporting: {value:"none"},
31
+ reportingformat: {value:"human"},
32
+ persist: {value:false},
33
+ ignoretimerpass: {value:false},
34
+ donotresettimer: {value:false},
35
+ thresholdaction: {value:"donothing"},
36
+ thresholdcount: {value:"0"},
37
+ thresholdaddtime: {value:"0"},
38
+ thresholdaddtimeunits: {value:"Second"},
39
+ heartbeatinterval: {value:"0"},
40
+ heartbeatintervalunits: {value:"Second"},
41
+ cooldownduration: {value:"0"},
42
+ cooldownunits: {value:"Second"}
43
+ },
44
+ inputs:1,
45
+ outputs:4,
46
+ icon: "stoptimer.png",
47
+ label: function() {
48
+ return this.name || this.duration + " " + this.units + " Timer";
49
+ },
50
+ labelStyle: function() {
51
+ return this.name?"node_label_italic":"";
52
+ },
53
+ paletteLabel: "Timer with Events",
54
+ outputLabels: ["Start","Stop","Query","Events"],
55
+ oneditprepare: function() {
56
+ if (this.durationType == null) {
57
+ this.durationType = "num";
58
+ }
59
+
60
+ $("#node-input-durationType").val(this.durationType);
61
+
62
+ $("#node-input-duration").typedInput({
63
+ default: 'num',
64
+ typeField: $("#node-input-durationType"),
65
+ types:['num','env']
66
+ });
67
+
68
+ $("#node-input-duration").typedInput('type', this.durationType);
69
+
70
+ $("#node-input-reporting").val(this.reporting);
71
+ $("#node-input-reportingformat").val(this.reportingformat);
72
+ $("#node-input-thresholdaction").val(this.thresholdaction);
73
+ $("#node-input-thresholdaddtimeunits").val(this.thresholdaddtimeunits);
74
+ $("#node-input-heartbeatintervalunits").val(this.heartbeatintervalunits);
75
+ $("#node-input-cooldownunits").val(this.cooldownunits);
76
+
77
+ function updateThresholdVisibility() {
78
+ // Threshold Count is the master switch, matching the
79
+ // runtime (thresholdcount <= 0 disables threshold logic
80
+ // regardless of the configured action). At 0 the action
81
+ // rows are hidden but the stored action is left untouched,
82
+ // so toggling the count off and back does not lose it.
83
+ let count = parseInt($("#node-input-thresholdcount").val(), 10);
84
+ let enabled = !isNaN(count) && count > 0;
85
+ if (!enabled) {
86
+ $("#threshold-action-row").hide();
87
+ $("#threshold-addtime-row").hide();
88
+ $("#threshold-addtimeunits-row").hide();
89
+ return;
90
+ }
91
+ $("#threshold-action-row").show();
92
+ let action = $("#node-input-thresholdaction").val();
93
+ if (action === "addtime") {
94
+ $("#threshold-addtime-row").show();
95
+ $("#threshold-addtimeunits-row").show();
96
+ } else {
97
+ $("#threshold-addtime-row").hide();
98
+ $("#threshold-addtimeunits-row").hide();
99
+ }
100
+ }
101
+
102
+ $("#node-input-thresholdaction").on("change", updateThresholdVisibility);
103
+ $("#node-input-thresholdcount").on("change input", updateThresholdVisibility);
104
+ updateThresholdVisibility();
105
+ }
106
+ });
107
+ </script>
108
+
109
+ <script type="text/x-red" data-template-name="timer-events">
110
+ <div class="form-row">
111
+ <i class="fa fa-clock-o"></i>
112
+ <label for="node-input-duration"> Timer</label>
113
+ <input type="hidden" id="node-input-durationType">
114
+ <input type="text" id="node-input-duration" style="text-align:end; width:270px !important">
115
+ </div>
116
+ <div class="form-row">
117
+ <i class="fa fa-bars"></i>
118
+ <label for="node-input-units"> Units</label>
119
+ <select id="node-input-units">
120
+ <option value="Millisecond">Milliseconds</option>
121
+ <option value="Second">Seconds</option>
122
+ <option value="Minute">Minutes</option>
123
+ <option value="Hour">Hours</option>
124
+ </select>
125
+ </div>
126
+ <div class="form-row">
127
+ <i class="fa fa-heartbeat"></i>
128
+ <label for="node-input-reporting"> Status Reporting</label>
129
+ <select id="node-input-reporting">
130
+ <option value="none">Never</option>
131
+ <option value="every_second">Every Second</option>
132
+ <option value="last_minute_seconds">Every Minute, Last minute by seconds</option>
133
+ </select>
134
+ </div>
135
+ <div class="form-row">
136
+ <label for="node-input-reportingformat"> Reporting format</label>
137
+ <select id="node-input-reportingformat">
138
+ <option value="human">HH:MM:SS</option>
139
+ <option value="seconds">Seconds</option>
140
+ <option value="minutes">Minutes</option>
141
+ <option value="hours">Hours</option>
142
+ </select>
143
+ </div>
144
+ <div class="form-row">
145
+ <i class="fa fa-floppy-o"></i>
146
+ <label style="width:auto" for="node-input-persist"> Resume timer on deploy/restart</label>
147
+ <input type="checkbox" id="node-input-persist" style="display:inline-block; width:auto; vertical-align:top;">
148
+ </div>
149
+ <div class="form-row">
150
+ <i class="fa fa-recycle"></i>
151
+ <label style="width:auto" for="node-input-ignoretimerpass"> Ignore incoming _timerpass</label>
152
+ <input type="checkbox" id="node-input-ignoretimerpass" style="display:inline-block; width:auto; vertical-align:top;">
153
+ </div>
154
+ <div class="form-row">
155
+ <i class="fa fa-lock"></i>
156
+ <label style="width:auto" for="node-input-donotresettimer"> Do Not Reset Timer on Subsequent Incoming Message</label>
157
+ <input type="checkbox" id="node-input-donotresettimer" style="display:inline-block; width:auto; vertical-align:top;">
158
+ </div>
159
+ <hr>
160
+ <div class="form-row">
161
+ <i class="fa fa-hashtag"></i>
162
+ <label for="node-input-thresholdcount"> Threshold Count</label>
163
+ <input type="number" id="node-input-thresholdcount" min="0" style="width:100px">
164
+ </div>
165
+ <div class="form-row" id="threshold-action-row">
166
+ <i class="fa fa-exclamation-triangle"></i>
167
+ <label for="node-input-thresholdaction"> Ignored Message Threshold Action</label>
168
+ <select id="node-input-thresholdaction">
169
+ <option value="donothing">Do Nothing</option>
170
+ <option value="stop">Stop</option>
171
+ <option value="pause">Pause</option>
172
+ <option value="reset">Restart Timer</option>
173
+ <option value="addtime">Add Time</option>
174
+ <option value="warning">Emit Warning</option>
175
+ </select>
176
+ </div>
177
+ <div class="form-row" id="threshold-addtime-row">
178
+ <i class="fa fa-plus-circle"></i>
179
+ <label for="node-input-thresholdaddtime"> Add Time Amount</label>
180
+ <input type="number" id="node-input-thresholdaddtime" min="1" style="width:100px">
181
+ </div>
182
+ <div class="form-row" id="threshold-addtimeunits-row">
183
+ <label for="node-input-thresholdaddtimeunits"> Add Time Units</label>
184
+ <select id="node-input-thresholdaddtimeunits">
185
+ <option value="Millisecond">Milliseconds</option>
186
+ <option value="Second">Seconds</option>
187
+ <option value="Minute">Minutes</option>
188
+ <option value="Hour">Hours</option>
189
+ </select>
190
+ </div>
191
+ <hr>
192
+ <div class="form-row">
193
+ <i class="fa fa-heart-o"></i>
194
+ <label for="node-input-heartbeatinterval"> Heartbeat Interval</label>
195
+ <input type="number" id="node-input-heartbeatinterval" min="0" style="width:100px">
196
+ </div>
197
+ <div class="form-row">
198
+ <label for="node-input-heartbeatintervalunits"> Heartbeat Units</label>
199
+ <select id="node-input-heartbeatintervalunits">
200
+ <option value="Millisecond">Milliseconds</option>
201
+ <option value="Second">Seconds</option>
202
+ <option value="Minute">Minutes</option>
203
+ <option value="Hour">Hours</option>
204
+ </select>
205
+ </div>
206
+ <hr>
207
+ <div class="form-row">
208
+ <i class="fa fa-snowflake-o"></i>
209
+ <label for="node-input-cooldownduration"> Cooldown Duration</label>
210
+ <input type="number" id="node-input-cooldownduration" min="0" style="width:100px">
211
+ </div>
212
+ <div class="form-row">
213
+ <label for="node-input-cooldownunits"> Cooldown Units</label>
214
+ <select id="node-input-cooldownunits">
215
+ <option value="Millisecond">Milliseconds</option>
216
+ <option value="Second">Seconds</option>
217
+ <option value="Minute">Minutes</option>
218
+ <option value="Hour">Hours</option>
219
+ </select>
220
+ </div>
221
+ <hr>
222
+ <div class="form-row">
223
+ <i class="fa fa-tag"></i>
224
+ <label for="node-input-name"> Name</label>
225
+ <input type="text" id="node-input-name" placeholder="Name"></input>
226
+ </div>
227
+ </script>
228
+
229
+ <script type="text/x-red" data-help-name="timer-events">
230
+ <p><B>General usage</B><br>
231
+ A countdown timer with four purpose-built outputs. Any incoming <code>msg</code> starts (or, if already running, restarts) the timer unless it carries a recognized control command in <code>msg.payload</code>. All control commands are <b>case insensitive</b>.
232
+ </p>
233
+ <p><B>Outputs</B><br>
234
+ <ul>
235
+ <li><b>1 - Start</b> — fires only when the timer transitions from stopped/expired into running. Never fires for a restart of an already-running timer, and never fires for a blocked/ignored start attempt (including one blocked by an active Cooldown).</li>
236
+ <li><b>2 - Stop</b> — fires only on a genuine stop (via the <code>stop</code> command) or a natural expiry. Never fires for a blocked/ignored stop attempt, and never fires again when a Cooldown period ends (see Cooldown below).</li>
237
+ <li><b>3 - Query</b> — fires in response to an incoming <code>query</code> message, or automatically on each Heartbeat tick (see below). Always carries a full status snapshot.</li>
238
+ <li><b>4 - Events</b> — fires for every other event, including duplicate copies of Start and Stop events. This is the only output where <code>msg.ignored</code> can be <code>true</code>, marking a command that was received but did not actually change the timer's state.</li>
239
+ </ul>
240
+ Output 1 and output 2 never carry an ignored message — anything that didn't truly happen only ever appears on output 4.
241
+ </p>
242
+ <p><B>Message properties on all outputs</B><br>
243
+ Every output message is a clone of the message that triggered it (or, for events with no live trigger of their own — natural expiry, a Heartbeat tick, a threshold action — a clone of the most recent Start/Restart message) with the following properties added:
244
+ <ul>
245
+ <li><code>msg.timerEvent</code> — the event type, see the list below</li>
246
+ <li><code>msg.timerState</code> — the current state of the timer: <code>running</code>, <code>paused</code>, <code>stopped</code>, <code>expired</code>, or <code>cooldown</code></li>
247
+ <li><code>msg.remainingTime</code> — the remaining time in milliseconds: live wall-clock remaining while <code>running</code>, frozen while <code>paused</code>, the time left in the Cooldown period while in <code>cooldown</code>, and <code>0</code> while idle (<code>stopped</code>/<code>expired</code>)</li>
248
+ <li><code>msg.timerDuration</code> — the current run's total duration in milliseconds</li>
249
+ <li><code>msg.elapsedTime</code> — elapsed time in milliseconds, defined per state: while <code>running</code>, wall-clock time since the current run started; while <code>paused</code>, frozen at the value it had at the moment of pause; while in <code>cooldown</code>, the time elapsed <i>into the Cooldown period</i> (so <code>elapsedTime + remainingTime</code> approximates the Cooldown duration); while idle (<code>stopped</code>/<code>expired</code>), <code>0</code> — there is no current run. The genuine <code>stopped</code> and <code>expired</code> events are the exception: they carry the run's <i>final</i> elapsed value, snapshotted the instant before the run ended, so a consumer can see how far the run got.</li>
250
+ <li><code>msg.ignoredCount</code> — the number of messages ignored during the current timer run</li>
251
+ <li><code>msg.lastIgnoredTime</code> — ISO 8601 timestamp of the last ignored message, or <code>null</code> if none</li>
252
+ <li><code>msg.doNotResetTimer</code> — current runtime lock state as a boolean</li>
253
+ <li><code>msg.disabled</code> — current disabled state as a boolean</li>
254
+ <li><code>msg.ignored</code> — <code>true</code> if this message was received but did not change the timer's state (always <code>false</code> on outputs 1, 2, and 3)</li>
255
+ <li><code>msg.source</code> — <code>"external"</code> if a live incoming message triggered this event, or <code>"internal"</code> if the node itself triggered it (a Heartbeat tick, a threshold action, or a persisted restore on deploy/restart)</li>
256
+ </ul>
257
+ </p>
258
+ <p><B>Event types (<code>msg.timerEvent</code>)</B><br>
259
+ <ul>
260
+ <li><code>started</code> — the timer transitioned from stopped/expired into running (output 1 + 4), or a blocked start attempt while disabled (output 4 only, <code>ignored:true</code>)</li>
261
+ <li><code>restarted</code> — an already-running or paused timer's remaining time was reset back to a fresh run (output 4 only). This covers a new message arriving while already running, as well as a blocked restart attempt while paused or locked (<code>ignored:true</code> in the blocked case)</li>
262
+ <li><code>stopped</code> — the timer was stopped via the <code>stop</code> command (output 2 + 4), or a redundant <code>stop</code> received while the timer was already idle — <code>stopped</code> or <code>expired</code> (output 4 only, <code>ignored:true</code>, zero state change)</li>
263
+ <li><code>expired</code> — the timer ran to completion naturally (output 2 + 4)</li>
264
+ <li><code>paused</code> — the timer was paused, or a blocked pause attempt (output 4 only)</li>
265
+ <li><code>resumed</code> — the timer was resumed, or a blocked resume attempt (output 4 only)</li>
266
+ <li><code>locked</code> — Do Not Reset Timer enabled at runtime, or a redundant lock attempt (output 4 only)</li>
267
+ <li><code>unlocked</code> — Do Not Reset Timer disabled at runtime, or a redundant unlock attempt (output 4 only)</li>
268
+ <li><code>disabled</code> — new timer starts blocked, or a redundant disable attempt (output 4 only)</li>
269
+ <li><code>enabled</code> — new timer starts allowed again, or a redundant enable attempt (output 4 only)</li>
270
+ <li><code>timeadjusted</code> — remaining time was adjusted, or a blocked/invalid adjust attempt. <code>msg.timeAdjusted</code> contains the amount (attempted or applied) in milliseconds (output 4 only)</li>
271
+ <li><code>timeset</code> — remaining time was set to an exact value, or a blocked/invalid attempt. <code>msg.timeSet</code> contains the value (attempted or applied) in milliseconds (output 4 only)</li>
272
+ <li><code>durationset</code> — the duration for future runs was set, or an invalid attempt. <code>msg.durationSet</code> contains the value (attempted or applied) in milliseconds (output 4 only)</li>
273
+ <li><code>warning</code> — a threshold Warning fired. This is never <code>ignored:true</code> — it is a deliberate, side-effect-free notification, not a blocked action (output 4 only)</li>
274
+ <li><code>query</code> — a status snapshot, either requested via a <code>query</code> message or produced by a Heartbeat tick (output 3 only)</li>
275
+ <li><code>cooldownstarted</code> — a Cooldown period began following a natural expiry (output 4 only, see Cooldown below)</li>
276
+ <li><code>cooldownended</code> — a Cooldown period completed naturally (output 4 only). This does <b>not</b> repeat <code>expired</code> on output 2 — expiry was already reported once when the countdown originally hit zero</li>
277
+ </ul>
278
+ </p>
279
+ <p><B>Control Commands</B><br>
280
+ All control commands are case insensitive. The following commands are supported:
281
+ <ul>
282
+ <li><code>stop</code> — cancels the timer immediately. <code>stop</code> is genuine whenever there is something alive to kill — a running timer, a paused timer, or an active Cooldown. Sent while the timer is truly idle (<code>stopped</code> or <code>expired</code>) it is a redundant command: routed to output 4 with <code>msg.ignored = true</code> and no state change whatsoever — no counter reset, no <code>expired</code> → <code>stopped</code> state change, and no arming of the <code>_timerpass</code> filter.</li>
283
+ <li><code>pause</code> — freezes the countdown at the current remaining time</li>
284
+ <li><code>resume</code> — restarts the countdown from the frozen point</li>
285
+ <li><code>query</code> — returns a full state snapshot on output 3 without affecting the timer</li>
286
+ <li><code>lock</code> — enables Do Not Reset Timer at runtime</li>
287
+ <li><code>unlock</code> — disables Do Not Reset Timer at runtime</li>
288
+ <li><code>disable</code> — prevents new timer starts. Everything else works normally. The current timer run continues if running.</li>
289
+ <li><code>enable</code> — re-enables new timer starts</li>
290
+ <li><code>adjusttime</code> — adds or subtracts time from the current remaining time (running or paused only). Requires <code>msg.adjusttime</code> in milliseconds. Optionally set <code>msg.adjusttimeunits</code> to override units.</li>
291
+ <li><code>settime</code> — sets the current remaining time to an exact value (running or paused only). Requires <code>msg.settime</code> in milliseconds. Optionally set <code>msg.settimeunits</code>. Must be positive.</li>
292
+ <li><code>setduration</code> — sets the timer duration for all future runs. Requires <code>msg.setduration</code> in milliseconds. Optionally set <code>msg.setdurationunits</code>. Must be positive. Works in all states.</li>
293
+ </ul>
294
+ For all three time commands, a missing or non-numeric value (e.g. <code>"abc"</code>, an empty string, or the property absent entirely) is rejected with no state change: the command is routed to output 4 with <code>msg.ignored = true</code>. The attempted value rides along on <code>msg.timeAdjusted</code> / <code>msg.timeSet</code> / <code>msg.durationSet</code> so a downstream consumer can see what was rejected — as the raw unconvertible value, or <code>null</code> if the property was absent. Numeric-but-invalid values (e.g. <code>settime</code> ≤ 0) are likewise rejected as <code>ignored:true</code>, carrying the converted milliseconds.
295
+ </p>
296
+ <p><B>Heartbeat</B><br>
297
+ Optionally configure a <i>Heartbeat Interval</i> to have the node automatically produce a status snapshot on output 3 (Query) at a fixed, regular cadence while a timer run is active — useful for external monitoring or watchdog systems that need to confirm the node is functioning correctly over long running timers.
298
+ <ul>
299
+ <li>Set to <code>0</code> (default) to disable</li>
300
+ <li>The heartbeat starts the moment the timer starts and fires on a fixed schedule</li>
301
+ <li>Each tick produces a normal Query-output message with <code>msg.source = "internal"</code>, so it's indistinguishable in shape from a manually requested query — inspect <code>msg.timerState</code> to see whether it landed while running or paused</li>
302
+ <li>The heartbeat fires whether the timer is running <b>or</b> paused</li>
303
+ <li>The heartbeat schedule is <b>not</b> affected by pause, resume, adjusttime, settime, or threshold actions — it ticks independently on its own fixed interval once started. A <i>restart</i> of the timer (a new message while already running) does restart the heartbeat schedule from that moment, since a restart begins a new run.</li>
304
+ <li>The heartbeat stops when the timer is stopped or expires</li>
305
+ <li><code>disable</code> does not affect an in-progress run's heartbeat — it continues until the run stops or expires naturally</li>
306
+ <li>After a persisted restart, the heartbeat restarts fresh from that point rather than resuming the original schedule</li>
307
+ </ul>
308
+ </p>
309
+ <p><B>Cooldown</B><br>
310
+ Optionally configure a <i>Cooldown Duration</i> to automatically prevent the timer from being started again for a fixed period of time after it naturally expires — for example, running for 6 hours and then refusing to start again for at least 4 hours afterward.
311
+ <ul>
312
+ <li>Set to <code>0</code> (default) to disable</li>
313
+ <li>Cooldown begins <b>only</b> after a natural expiry — sending <code>stop</code> ends the timer immediately with no cooldown</li>
314
+ <li>The sequence is: <code>running</code> → <code>expired</code> (output 2 + 4, fires once) → <code>cooldown</code> (output 4: <code>cooldownstarted</code>) → cooldown ends, settling back to an idle <code>expired</code> state (output 4: <code>cooldownended</code> — <code>expired</code> is <b>not</b> re-fired on output 2)</li>
315
+ <li>While in Cooldown, new start attempts are blocked and routed to output 4 with <code>msg.timerEvent = "started"</code> and <code>msg.ignored = true</code> — the same treatment as a blocked start while <i>Disabled</i></li>
316
+ <li><i>Disabled</i> and Cooldown are independent blocking conditions — either one blocks a new start regardless of the other, and enabling/disabling the node has no effect on an active Cooldown</li>
317
+ <li>Sending <code>stop</code> during Cooldown cancels it immediately and fires a normal <code>stopped</code> event (output 2 + 4) — this is the only way to cut a Cooldown short</li>
318
+ <li>Threshold actions do not fire during Cooldown — threshold logic is scoped to a genuinely running timer, which Cooldown is not</li>
319
+ <li>Heartbeat continues ticking normally during Cooldown</li>
320
+ <li>A <code>query</code> during Cooldown reports <code>msg.timerState = "cooldown"</code> with <code>msg.remainingTime</code> reflecting the time left in the Cooldown period</li>
321
+ <li>The node status shows a short <code>Cooldown: HH:MM:SS</code> (yellow dot) using the <i>Reporting</i> cadence and format configured above — no ignored-count detail is shown during Cooldown. If the node is also Disabled, the status simply shows <code>Disabled</code></li>
322
+ <li>Cooldown is persisted and resumed across deploy/restart if <i>Resume timer on deploy/restart</i> is enabled, using the same restore logic as a running or paused timer</li>
323
+ </ul>
324
+ </p>
325
+ <p><B>Status Reporting</B><br>
326
+ The <i>Status Reporting</i> option controls only the colored status text shown below the node in the editor — it no longer produces any output message (that role is served by the Query output, manually or via Heartbeat). Options:
327
+ <ul>
328
+ <li>Never (default)</li>
329
+ <li>Every Second</li>
330
+ <li>Every Minute, Last minute by seconds</li>
331
+ </ul>
332
+ The last option works as follows:
333
+ <ul>
334
+ <li>While there is more than 1 minute remaining, the status will decrement every minute. At the 1 minute point, it switches to updating every second.</li>
335
+ <li>The exception to this rule is if your duration is not a minute increment. In that case, the first update will be for the partial minute, after which it operates as noted above. (for example: 2.5 minutes will decrement to 2 minutes, then 1 minute, then every second down to zero)</li>
336
+ </ul>
337
+ The format is defined by the <i>Reporting Format</i> option: hh:mm:ss (default), or the total number of seconds, minutes, or hours.
338
+ </p>
339
+ <p><B>Querying the timer</B><br>
340
+ Send a message with <code>msg.payload</code> of <code>query</code> (any case) to get a full snapshot of the current timer state on output 3. The timer is not affected in any way by a query message. <code>msg.source</code> will be <code>"external"</code> for a manually requested query, or <code>"internal"</code> for one produced by a Heartbeat tick.
341
+ </p>
342
+ <p><B>Overriding the node via incoming messages</B><br>
343
+ If the input contains <code>msg.delay</code>, then the delay will be <code>msg.delay</code> units of time, where the units are whatever the units are defaulted to in the node itself. Fractional values are supported (e.g. <code>2.5</code> minutes). In the absence of a <code>msg.delay</code>, or a value that is not strictly numeric — a non-numeric string like <code>"5s"</code> or <code>"abc"</code>, an empty string, a boolean, and so on — the value configured within the node will be used. If the value of <code>msg.delay</code> is less than 0, then 0 is used (the timer fires immediately).
344
+ </p>
345
+ <p>
346
+ If the input contains <code>msg.units</code>, with a value of "milliseconds", "seconds", "minutes" or "hours" then that will override what is defaulted in the node. Both singular and plural forms are accepted and the value is case insensitive. In the absence of a <code>msg.units</code>, or an unknown string in <code>msg.units</code>, the units configured within the node will be used. In the case of an unknown string, a warning message will appear in the Debug logs.
347
+ </p>
348
+ <p><b>Pausing and Resuming</b><br>
349
+ The timer can be paused by sending a message with <code>msg.payload</code> of <code>pause</code> (any case). While paused:
350
+ <ul>
351
+ <li>The countdown is frozen at the remaining time</li>
352
+ <li>The node status shows <code>Paused: HH:MM:SS</code> with a yellow indicator</li>
353
+ <li>Any incoming message other than recognized control commands will be treated as a blocked restart attempt and routed to output 4 with <code>msg.timerEvent = "restarted"</code> and <code>msg.ignored = true</code></li>
354
+ <li>Sending another <code>pause</code> message while already paused is routed to output 4 with <code>msg.ignored = true</code></li>
355
+ <li>Sending a <code>pause</code> message when the timer is not running is routed to output 4 with <code>msg.ignored = true</code>, and the current status is restored</li>
356
+ </ul>
357
+ The timer can be resumed by sending a message with <code>msg.payload</code> of <code>resume</code> (any case). On resume:
358
+ <ul>
359
+ <li>The countdown restarts from where it was frozen</li>
360
+ <li>Output 4 sends <code>msg.timerEvent = "resumed"</code></li>
361
+ <li>Sending a <code>resume</code> message when the timer is not paused is routed to output 4 with <code>msg.ignored = true</code>, and the current status is restored</li>
362
+ </ul>
363
+ If the <i>Resume timer on deploy/restart</i> option is enabled and the timer is paused when Node-RED restarts or the flow is redeployed, the timer will restore as paused at the same remaining time — a paused timer is frozen, so Node-RED downtime is <b>not</b> deducted from it. The frozen elapsed time is restored independently as well (so a <code>settime</code> issued while paused survives a restart without distorting the elapsed value).
364
+ </p>
365
+ <p><b>Disable and Enable</b><br>
366
+ The node can be disabled to prevent new timer starts while allowing the current run to complete:
367
+ <ul>
368
+ <li><code>disable</code> (any case) — prevents new timer starts. The current timer run continues if running, including any active heartbeat. The timer can still be stopped, paused, resumed, queried, locked, unlocked, and time adjusted while disabled. A redundant disable is routed to output 4 with <code>msg.ignored = true</code>.</li>
369
+ <li><code>enable</code> (any case) — re-enables new timer starts. A redundant enable is routed to output 4 with <code>msg.ignored = true</code>.</li>
370
+ </ul>
371
+ When disabled, the node status shows a grey ring with <code>Disabled | </code> prefixed to the normal status text. The disabled state is persisted to disk if <i>Resume timer on deploy/restart</i> is enabled.
372
+ <br><br>
373
+ A blocked start attempt while disabled is routed to output 4 with <code>msg.timerEvent = "started"</code> and <code>msg.ignored = true</code> — it is treated as a blocked true start (not a restart), since the timer is idle when this happens.
374
+ <br><br>
375
+ <b>Use case:</b> A dehumidifier runs for 6 hours (timer 1). When it expires, send <code>disable</code> to the node and start a 4 hour rest timer (timer 2). Any start messages received during the 4 hour rest are blocked. When the 4 hour rest expires, send <code>enable</code> to allow the next cycle to start.
376
+ </p>
377
+ <p><b>Lock and Unlock</b><br>
378
+ The Do Not Reset Timer behavior can be toggled at runtime without changing the node configuration:
379
+ <ul>
380
+ <li><code>lock</code> (any case) — enables Do Not Reset Timer. Subsequent messages while running are treated as blocked restart attempts and routed to output 4. The ignored count resets to 0. A redundant lock (already locked) is itself routed to output 4 with <code>msg.ignored = true</code>.</li>
381
+ <li><code>unlock</code> (any case) — disables Do Not Reset Timer. Subsequent messages will reset the timer normally. The ignored count resets to 0. A redundant unlock is itself routed to output 4 with <code>msg.ignored = true</code>.</li>
382
+ </ul>
383
+ Both commands work in any timer state. The lock state is persisted to disk if <i>Resume timer on deploy/restart</i> is enabled.
384
+ </p>
385
+ <p><b>Ignored Message Threshold Action</b><br>
386
+ When <i>Do Not Reset Timer on Subsequent Incoming Message</i> is enabled, you can configure an automatic action to take when the ignored message count reaches a configured threshold:
387
+ <ul>
388
+ <li><b>Do Nothing (default)</b> — no action taken, count increments indefinitely</li>
389
+ <li><b>Stop</b> — stops the timer when the threshold is reached</li>
390
+ <li><b>Pause</b> — pauses the timer when the threshold is reached</li>
391
+ <li><b>Restart Timer</b> — restarts the timer from the original duration (identical in effect to a message-triggered restart, just internally sourced)</li>
392
+ <li><b>Add Time</b> — adds a configured amount of time to the timer</li>
393
+ <li><b>Emit Warning</b> — sends a warning on output 4 without affecting the timer</li>
394
+ </ul>
395
+ For all actions except Do Nothing and Emit Warning, the ignored count resets to 0 after the action fires. If the timer is paused when Restart Timer or Add Time fires, the remaining time is updated but the timer stays paused until a <code>resume</code> message is received.
396
+ <br><br>
397
+ <b>Scope:</b> threshold actions fire only against an <i>active run</i> — a timer that is <code>running</code> or <code>paused</code>. They never fire while the timer is idle, Disabled, or in Cooldown. Blocked start attempts while Disabled or in Cooldown still increment the visible ignored count (useful for monitoring), but that count can never trip a threshold action. Redundant commands (a <code>lock</code> while already locked, a <code>disable</code> while already disabled, a <code>stop</code> while idle, and so on) never touch the ignored count at all.
398
+ <br><br>
399
+ <b>Threshold Count is the master switch:</b> set it to <code>0</code> (the default) to disable threshold actions entirely — the Action dropdown is hidden in the editor while the count is 0, and the runtime ignores the configured action regardless.
400
+ <br><br>
401
+ Every threshold action fires on output 4 with <code>msg.source = "internal"</code> and <code>msg.ignored = false</code> (Emit Warning is never an ignored event — it is a deliberate notification). A threshold action that stops the timer also fires on output 2, same as any other stop.
402
+ </p>
403
+ <p><b>Special Note on Milliseconds</b><br>
404
+ While you can set Milliseconds, I would not rely on the accuracy for anything critical.
405
+ </p>
406
+ <p><b>Resume timer on deploy/restart</b><br>
407
+ This option is <b>DISABLED</b> by default. If you <i>ENABLE</i> it (check the checkbox) then if the timer is running and you re-Deploy the flow, or restart Node-RED, then the timer will automatically restart itself where it should be. What does that mean? A couple of examples will help here.
408
+ <ul>
409
+ <li>If you had a 10 minute timer running, with 6 minutes elapsed (ie: 4 minutes left) and you hit Deploy, normally the timer would no longer be running, but if you have this feature enabled, the timer will continue running from the 6 minute mark (ie: counting down 4 more minutes and then trigger). This is treated as a Start (output 1 + 4), since the timer is coming back from a stopped/idle state on deploy/restart.</li>
410
+ <li>If you had a 10 minute timer running, with 6 minutes elapsed (ie: 4 minutes left) and you <i>stopped</i> Node-RED for 2 minutes and then restarted it, normally the timer would no longer be running, but if you have this feature enabled, the timer will continue running from the 8 minute mark (6 minutes from the original run + 2 minutes of Node-RED downtime) -- counting down 2 more minutes and then trigger.</li>
411
+ <li><b>Special Case</b> If on restart or re-Deploy, there is less than 3 seconds remaining on the timer (or if the timer should have elapsed already) then the timer is set to a random amount between 3 and 8 seconds. This helps to ensure that anything else that needs to initialize before the timer triggers has a chance to do so. It also helps so that if you happen to have a lot of timers, they don't all trigger at once and flood unsuspecting nodes/devices.</li>
412
+ </ul>
413
+ <br>
414
+ A running restore is a <b>continuation</b> of the interrupted run, not a new one: the run's original duration (<code>msg.timerDuration</code>), its accumulated ignored count and last-ignored timestamp, and its elapsed time (including the Node-RED downtime, per the examples above) all survive the restore. Only the Start event on output 1 treats it as a start — that signal exists for downstream consumers, who see the timer coming back from nothing.
415
+ <br><br>
416
+ The node's freshly-deployed configuration always wins over persisted settings: changing <i>Status Reporting</i> or <i>Reporting format</i> and redeploying mid-run takes effect immediately on restore.
417
+ <br><br>
418
+ <b>Limitation:</b> the ignored count is written to disk only when the next state-changing event triggers a save — ignored messages themselves do not write. A hard crash (as opposed to a clean redeploy or shutdown) can therefore lose ignored counts accumulated since the last save. Timing is never affected, and a lost count can at worst delay a threshold action past its intended trigger point after recovery.
419
+ <br><br>
420
+ This persistence is <b>not</b> related to "Persistent Context" (the contextStorage option in <code>settings.js</code>). When the "Resume timer" option is enabled in the node, the node will store timer related information in a <code>timerevents-timers</code> subdirectory of <i>userDir</i> (where <i>userDir</i> is defined in <code>settings.js</code>). If <i>userDir</i> is not explicitly defined, it defaults to a directory called <code>.node-red</code> in your home user directory. The files in this directory will be created/destroyed as needed by the node.
421
+ </p>
422
+ <p><b>What is <code>_timerpass</code></b><br>
423
+ <code>_timerpass</code> is a property added to the outgoing <code>msg</code> when the timer starts or restarts.
424
+ <code>_timerpass</code> is set to <code>true</code> on that message.
425
+ <br><br>
426
+ <b>What does <code>_timerpass</code> do?</b><br>
427
+ If this node has at any point been stopped by a <i>genuine</i> <code>stop</code> — one that actually stopped something alive (a running timer, a paused timer, or an active Cooldown) AND <br>
428
+ If this node has not received a message with _timerpass not set since that time THEN <br>
429
+ any incoming message that has the _timerpass=true property will die within this node with no output.
430
+ <br><br>
431
+ A redundant <code>stop</code> (sent while the timer was already idle — <code>stopped</code> or <code>expired</code>) does <b>not</b> arm this filter; it is an ignored command with no state change.
432
+ <br><br>
433
+ <b>How does Timer with Events handle this?</b><br>
434
+ <i>Ignore incoming _timerpass</i> in the node config dialog. If enabled, the node will ignore the presence of the <code>_timerpass</code> property on an incoming message and will process it normally.
435
+ </p>
436
+ <p><b>Do Not Reset Timer on Subsequent Incoming Message</b><br>
437
+ This option is <b>DISABLED</b> by default. If you <i>ENABLE</i> it (check the checkbox) then while the timer is running, any subsequent incoming messages (other than control commands) will be ignored and the timer will continue running undisturbed. The first message always starts the timer normally. This can also be toggled at runtime using the <code>lock</code> and <code>unlock</code> commands.
438
+ <br><br>
439
+ When a message is ignored this way, it is routed to output 4 with <code>msg.timerEvent = "restarted"</code> and <code>msg.ignored = true</code>, plus the standard message properties described above.
440
+ <br><br>
441
+ When this option is enabled, the node status will show:<br>
442
+ <code>Remaining: 00:04:32 | Ignored: 3, Last: Jun 27 14:22:05</code>
443
+ </p>
444
+ </script>