jw-automator 2.0.0 โ†’ 3.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.
package/README.md CHANGED
@@ -1,278 +1,461 @@
1
- # jw-automator
2
-
3
- Package for automating events. Designed for automating IoT devices, such as logging the temperature every 15 minutes, or turning lights on/off at certain times, but could also be used as a full calendaring system or to schedule anything else you might need to happen at certain times.
4
-
5
- Because of it's intended use, the minimum interval is 1 second. If you need to automate something that runs more often then that you should probably just use the native __setInterval()__ function. That being said there is a way to leverage the extra features of jw-automator for millisecond-interval level events, which will be discussed later in the readme.
6
-
7
- ## Basic Usage ##
8
- ``` javascript
9
- //Import the library
10
- var Auto = require('jw-automator');
11
-
12
- //Create the automator
13
- var automator = new Auto.automator();
14
-
15
- //add a function that takes a simple payload (you can only use a single param)
16
- automator.addFunction('testFunc', (msg) => {
17
- console.log('Automator says: ' + msg);
18
- });
19
-
20
- //Add an action to the automator
21
- automator.addAction({
22
- name: 'hello', //The name of this action
23
- cmd: 'testFunc', //cmd to call
24
- payload: 'Hello World', //payload to send to cmd
25
- repeat: { //if you don't provide the repeat param the action will only run once, immediately
26
- type:'second', // second/minute/hour/day/week/month/year/weekday/weekend
27
- interval: 5, //how many of the type to skip, 2=every other time
28
- }
29
- });
30
- ```
1
+ # ๐Ÿ“š **jw-automator v3**
31
2
 
32
- Result:
33
- Every 5 seconds the console will log 'Automator says: Hello World'
34
-
35
- ## Constructor Options ##
36
- ``` javascript
37
- var automator = new Auto.automator(options);
38
- ```
39
- __options.save__
40
- _boolean_
41
- Should the automator save its current state as a local JSON file.
42
- Default = true
3
+ ### A resilient, local-time, 1-second precision automation scheduler for Node.js
43
4
 
44
- The save file will be overwritten by any addAction() calls in your code, so be aware of that. The automator is either designed to be used in a code-focused way, i.e. a simple program to log temperature data, or a user-controlled way, i.e. the backend to allow end-users to setup smart-light routines. As such the save file should probably only be used if you expect end-users to modify the actions.
5
+ **Human-friendly recurrence rules. Offline catch-up. DST-safe. Predictable. Extensible.**
45
6
 
46
7
  ---
47
8
 
48
- __options.saveFile__
49
- _string_
50
- Alternate path for the save file.
51
- Default = '.actions.json'
9
+ ## โญ๏ธ Overview
52
10
 
53
- ---
11
+ **jw-automator** is a robust automation engine designed for small devices, home automation hubs, personal servers, and Node.js environments where **correctness, resilience, and local-time behavior** matter more than millisecond precision.
54
12
 
55
- ## Public Functions ##
13
+ Where traditional cron falls short โ€” missed executions, poor DST handling, limited recurrence, lack of catch-up semantics โ€” **jw-automator** provides a predictable, human-centric scheduling model:
56
14
 
57
- __start__
58
- Starts the Automator
15
+ * **1-second granularity** with zero drift
16
+ * **Local calendar semantics** (weekday/weekend, monthly, yearly)
17
+ * **Configurable DST policies** (fall-back once/twice)
18
+ * **Offline resiliency & catch-up logic**
19
+ * **Buffered/unBuffered execution policies**
20
+ * **Rich introspection and event lifecycle**
21
+ * **Meta-actions** that can dynamically create/update other actions
22
+ * **Pluggable persistence** (file, memory, custom storage)
23
+ * **Deterministic step engine** suitable for simulation/testing
59
24
 
60
- ---
25
+ This makes jw-automator ideal for:
26
+
27
+ * Small Raspberry Pi home automation hubs
28
+ * IoT applications
29
+ * Sensor sampling / periodic readings
30
+ * Daily/weekly routines
31
+ * "Smart home" orchestrations
32
+ * Systems that must *survive restarts, reboots, offline gaps, and DST transitions*
61
33
 
62
- __getActions__
63
- Returns a listing of the current actions and their states
34
+ jw-automator v3 is a **clean-room re-architecture** of the original library, keeping its best ideas while formalizing its semantics, improving correctness, and providing a crisp developer experience.
64
35
 
65
36
  ---
66
37
 
67
- __removeActionByID__
68
- Removes an action by the internal ID. You must use getActions() to determine the ID.
69
- _params_
70
- 1. ID _number_: The ID number for the action
38
+ ## ๐Ÿš€ Quick Start
71
39
 
72
- ---
40
+ ### Installation
73
41
 
74
- __removeActionByName__
75
- Removes an action by the name you provided in the .name option when you created it.
76
- _params_
77
- 1. name _string_: The action name
42
+ ```bash
43
+ npm install jw-automator
44
+ ```
78
45
 
79
- ---
46
+ ### Basic Usage
80
47
 
81
- __executeActionByID__
82
- Forces a given action to run immediately.
83
- _params_
84
- 1. ID _number_: the action ID
85
- 2. increment _boolean_: if true, this run will count towards the limit and count. Default = false
48
+ ```js
49
+ const Automator = require('jw-automator');
86
50
 
87
- ---
51
+ // Create an automator with file-based persistence
52
+ const automator = new Automator({
53
+ storage: Automator.storage.file('./actions.json')
54
+ });
88
55
 
89
- __executeActionByName__
90
- Forces a given action to run immediately.
91
- _params_
92
- 1. name _string_: the action name
93
- 2. increment _boolean_: if true, this run will count towards the limit and count. Default = false
56
+ // Register a command function
57
+ automator.addFunction('turnLightOn', function(payload) {
58
+ console.log('Turning light on');
59
+ });
94
60
 
95
- ---
61
+ // Add an action
62
+ automator.addAction({
63
+ name: 'Morning Lights',
64
+ cmd: 'turnLightOn',
65
+ date: new Date('2025-05-01T07:00:00'),
66
+ payload: null,
67
+ unBuffered: false,
68
+ repeat: {
69
+ type: 'day',
70
+ interval: 1,
71
+ limit: null,
72
+ endDate: null,
73
+ dstPolicy: 'once'
74
+ }
75
+ });
96
76
 
97
- __getActionsInRange__
98
- Generates an array of all the action objects that would run during a particular range of time. Every time an action will run will have an index in an array. This is useful if you're using the automator as the backend of a calendaring system and you want to display everything that will happen in a period of time, like for a week/month calendar display. Because actions can be set to repeat for a given limit or until a given date has occurred, the only way to generate this information is to simulate the actions and see what happens, as such it may take a few CPU cycles to generate the information. Therefore, this routine runs asynchronously and a callback must be used to retrieve the result.
99
- _params_
100
- 1. start _Date_: the date/time to start the range
101
- 2. end _Date_: the date/time to end the range
102
- 3. callback _function_: the callback function to run, returns an array with every action that will run, and the time it will run
77
+ // Start the scheduler
78
+ automator.start();
79
+ ```
103
80
 
104
81
  ---
105
82
 
106
- __updateActionByID__
107
- Updates an action by it's ID. Use to change an action in some way, like to modify it's interval or end date, etc.
108
- _params_
109
- 1. ID _number_: the action ID
110
- 2. newAction _object_: a new action object. It does not need to be a complete action, this will just overwrite the existing action with the new values you provide.
83
+ ## ๐Ÿ”ฅ Features
111
84
 
112
- ---
85
+ ### 1. **True 1-Second Precision**
86
+
87
+ * Scheduler tick interval is fixed at **1 second**.
88
+ * Execution times are aligned to the nearest whole second.
89
+ * No promise of sub-second timing (by design).
90
+ * Ideal for low-power hardware prone to event-loop delays.
113
91
 
114
- __updateActionByName__
115
- Updates an action by it's name. Use to change an action in some way, like to modify it's interval or end date, etc.
116
- _params_
117
- 1. name _string_: the action name
118
- 2. newAction _object_: a new action object. It does not need to be a complete action, this will just overwrite the existing action with the new values you provide.
92
+ > **Why?**
93
+ > A scheduler that *promises less* is dramatically more reliable.
119
94
 
120
95
  ---
121
96
 
122
- __addAction__
123
- Adds a new action to the automator.
124
- _params:_
125
- 1. action _object_: The action object, see below for details.
97
+ ### 2. **Human-Friendly Recurrence Rules**
126
98
 
127
- ---
99
+ Each action can specify a recurrence like:
128
100
 
129
- ## Action Options ##
101
+ ```js
102
+ repeat: {
103
+ type: 'weekday', // or: second, minute, hour, day, week,
104
+ // month, year, weekend
105
+ interval: 1, // every N occurrences
106
+ limit: null, // optional max count
107
+ endDate: null, // optional cutoff date
108
+ dstPolicy: 'once', // or 'twice'
109
+ }
110
+ ```
130
111
 
131
- ---
112
+ Examples:
132
113
 
133
- __name__ _required_
134
- _string_
135
- The name of the action.
114
+ * Every day at 7:00 AM
115
+ * Every 15 minutes
116
+ * Every weekend at 10:00
117
+ * Every month on the 1st
118
+ * Every weekday at market open
119
+ * Once per second for 5 minutes (limit-based)
136
120
 
137
121
  ---
138
122
 
139
- __date__
140
- _Date_
141
- The first time the action should run, leave blank or set to null to run immediately. The date can be in the past if needed, useful if the action is only supposed to run a set number of times and it should have started running already.
123
+ ### 3. **Local-Time First, DST-Aware**
142
124
 
143
- ---
125
+ jw-automator's recurrence rules operate in **local wall-clock time**, not UTC.
144
126
 
145
- __cmd__ _required_
146
- _string_
147
- The name of the function to run, must match a name added with the addAction() command.
127
+ This means:
148
128
 
149
- ---
129
+ * "7:00 AM" always means **local** 7:00 AM.
130
+ * Weekdays/weekends follow the user's locale.
131
+ * DST transitions are **explicit and predictable**:
150
132
 
151
- __payload__
152
- _any_
153
- An immutable payload to send the command. This is useful when you are using a command that is used by more than one action. As an example, if you have a function that logs some sort of data from various sensors, you could use the payload param to indicate which sensor to log. Honestly though, it's probably better to put whatever information you need in the automator action itself and then use that to call your theoretical logging function.
133
+ * **Spring forward:** missing hour handled via buffered/unBuffered rules
134
+ * **Fall back:** user chooses `dstPolicy: 'once' | 'twice'`
135
+
136
+ This avoids cron's silent-but-surprising behaviors.
154
137
 
155
138
  ---
156
139
 
157
- __unBuffered__
158
- _boolean_
159
- In a perfect world your action will run exactly when it is supposed to. In the real world it is possible that your CPU will be busy with other tasks during the entire second that the action was supposed to run. By default actions are buffered, so the action will run as soon as possible, which for most intended uses is what would be wanted. For example, if your action was meant to turn on the living room lights at exactly 9:00:00am, but due to CPU overhead they actually got turned on at 9:00:01am that would be fine, and desireable. There may be some cases, for example if you were running an action every single second, that missing a time might be better than running the same action several times at once when the CPU became free from whatever tasks were occupying it. If that's the case, set unBuffered=true
140
+ ### 4. **Resilient Offline Catch-Up**
160
141
 
161
- ---
142
+ If the device is offline or delayed (e.g., blocked by CPU load):
162
143
 
163
- __repeat__
164
- _object_
165
- The repeat object is the key part of the action as it specifies how the action should repeat! You can have an action that repeats forever at a specified interval, you can have an action that repeats until a certain date/time and/or you can have an action that repeats a certain number of times.
144
+ ```js
145
+ unBuffered: false // default: catch up missed executions
146
+ unBuffered: true // skip missed executions
147
+ ```
166
148
 
167
- ---
149
+ For example:
168
150
 
169
- __repeat.type__
170
- _string_
171
- Valid options: second, minute, hour, day, week, month, year, weekday, weekend
172
- Weekdays are Monday-Friday and Weekends are Saturday-Sunday. If you have an action repeat every 1 weekday it will repeat every day, Mon-Fri, then skip the weekend, then run again on Monday, etc.
173
- Each Weekday or Weekend (day) counts as one day, so if you have an action repeat every 2 weekend days starting on the first upcoming Saturday it will run Saturday, then will skip the next 2 weekend days (Sunday, Saturday next week) and run on Sunday (week 2), then skip the following Saturday and Sunday (week 3) and run again on Saturday (week 4).
151
+ * A job scheduled at 09:00 will still run when the device restarts at 10:00 (if buffered).
152
+ * A sequence of per-second readings will "compress" naturally after a delay.
174
153
 
175
- ---
154
+ This feature is ideal for:
176
155
 
177
- __interval__ _required_
178
- _number_
179
- The interval of the action. 1 = every time, 2=every other time, 3=every other 3rd time, etc.
180
- So, if you have an action starting at 6:00am, running every minute with an interval of 3, the action will run at:
181
- 6:00am
182
- 6:03am
183
- 6:06am
184
- 6:09am
185
- etc.
156
+ * Home automation logic ("turn heater off at 9 even if offline")
157
+ * Sensor sampling
158
+ * Data collection pipelines
186
159
 
187
160
  ---
188
161
 
189
- __limit__
190
- _number_
191
- how many times the action should run. If you want an action to run every 15 minutes for an hour you can set a limit of 5 and the action will run:
192
- 01:00
193
- 01:15
194
- 01:30
195
- 01:45
196
- 02:00
197
- And then stop.
198
- If you want an action to only run a certain number of times you can use __limit__ or __endDate__ as makes the most sense for your needs. Whichever comes first will be when the action stops.
162
+ ### 5. **Deterministic "Step Engine"**
199
163
 
200
- ___
164
+ The heart of jw-automator is a pure scheduling primitive:
201
165
 
202
- __endDate__
203
- _Date_
204
- The date and time the action should stop.
205
- If you want an action to only run a certain number of times you can use __limit__ or __endDate__ as makes the most sense for your needs. Whichever comes first will be when the action stops.
166
+ ```
167
+ step(state, lastTick, now) โ†’ { newState, events }
168
+ ```
169
+
170
+ This powers:
171
+
172
+ * Real-time ticking
173
+ * Offline catch-up
174
+ * Future schedule simulation
175
+ * Testing
176
+ * Meta-scheduling (actions that schedule other actions)
177
+
178
+ Because `step` is deterministic, you can:
179
+
180
+ * Test schedules without time passing
181
+ * Generate "what would happen tomorrow"
182
+ * Debug recurrence rules
183
+ * Build custom visual schedulers
206
184
 
207
185
  ---
208
186
 
209
- __count__
210
- _number_
211
- This is how many times the action has already run. Only useful if a limit is set and you want to override the default starting count. I can't think of any reason to do this outside of a debug environment.
212
- __NOTE:__ If you specify a limit and a start date in the past, the automator will simulate all the times the action should have run without actually running it, so if you create an action on Wednesday that is set to have started on Monday and should run every day, 5 times, it will run on Wednesday (today), Thursday and Friday. If you set a different count, like say 0, then it will run Wed,Thurs, Fri, Sat, Sun instead since you overrode the count.
187
+ ### 6. **Meta-Actions (Actions that Create Actions)**
188
+
189
+ jw-automator treats actions as **data**, enabling higher-order patterns:
190
+
191
+ * A daily 7:00 AM action can spawn a sequence of 60 one-per-second actions.
192
+ * A monthly billing action can create daily reminder actions.
193
+ * A multi-step animation (e.g., dimming a light) can create timed sub-actions.
194
+
195
+ Actions have a `repeat.count` that can be pre-set or manipulated intentionally.
196
+
197
+ This makes jw-automator more like a *mini automation runtime* than just a cron clone.
213
198
 
214
199
  ---
215
200
 
216
- __Here is a complete copy/paste version of the complete action object to use in your code:__
217
- ```javascript
218
- var action = {
219
- name: '', //user definable
220
- date: null, //next time the action should run, set null for immediately
221
- cmd: null, //function to call
222
- payload: null, //payload to send to function
223
- unBuffered: null, //when true actions missed due to sync delay will be skipped
224
- repeat: { //set this to null to only run the action once, alternatively set limit to 1
225
- type:'minute', // second/minute/hour/day/week/month/year/weekday/weekend
226
- interval: 1, //how many of the type to skip, 3=every 3rd type
227
- count: 0, //number of times the action has run, 0=hasn't run yet
228
- limit: null, //number of times the action should run, false means don't limit
229
- endDate: null //null = no end date
201
+ ## ๐Ÿ“ API Reference
202
+
203
+ ### Constructor
204
+
205
+ ```js
206
+ new Automator(options)
207
+ ```
208
+
209
+ Options:
210
+ * `storage` - Storage adapter (default: memory)
211
+ * `autoSave` - Auto-save state (default: true)
212
+ * `saveInterval` - Save interval in ms (default: 5000)
213
+
214
+ ### Methods
215
+
216
+ #### `start()`
217
+ Start the scheduler.
218
+
219
+ #### `stop()`
220
+ Stop the scheduler and save state.
221
+
222
+ #### `addFunction(name, fn)`
223
+ Register a command function.
224
+
225
+ ```js
226
+ automator.addFunction('myCommand', function(payload, event) {
227
+ console.log('Executing command with payload:', payload);
228
+ });
229
+ ```
230
+
231
+ #### `addAction(actionSpec)`
232
+ Add a new action. Returns the action ID.
233
+
234
+ ```js
235
+ const id = automator.addAction({
236
+ name: 'My Action',
237
+ cmd: 'myCommand',
238
+ date: new Date('2025-05-01T10:00:00'),
239
+ payload: { data: 'value' },
240
+ unBuffered: false,
241
+ repeat: {
242
+ type: 'hour',
243
+ interval: 2,
244
+ limit: 10,
245
+ dstPolicy: 'once'
246
+ }
247
+ });
248
+ ```
249
+
250
+ #### `updateActionByID(id, updates)`
251
+ Update an existing action.
252
+
253
+ ```js
254
+ automator.updateActionByID(1, {
255
+ name: 'Updated Name',
256
+ repeat: { type: 'day', interval: 1 }
257
+ });
258
+ ```
259
+
260
+ #### `updateActionByName(name, updates)`
261
+ Update all actions with the given name. Returns the number of actions updated.
262
+
263
+ ```js
264
+ automator.updateActionByName('My Action', {
265
+ payload: { newData: 'newValue' }
266
+ });
267
+ ```
268
+
269
+ #### `removeActionByID(id)`
270
+ Remove an action by ID.
271
+
272
+ #### `removeActionByName(name)`
273
+ Remove all actions with the given name.
274
+
275
+ #### `getActions()`
276
+ Get all actions (deep copy).
277
+
278
+ #### `getActionsByName(name)`
279
+ Get actions by name.
280
+
281
+ #### `getActionByID(id)`
282
+ Get a specific action by ID.
283
+
284
+ #### `getActionsInRange(startDate, endDate, callback)`
285
+ Simulate actions in a time range.
286
+
287
+ ```js
288
+ const events = automator.getActionsInRange(
289
+ new Date('2025-05-01'),
290
+ new Date('2025-05-07')
291
+ );
292
+
293
+ console.log(events); // Array of scheduled events
294
+ ```
295
+
296
+ #### `describeAction(id)`
297
+ Get a human-readable description of an action.
298
+
299
+ ### Events
300
+
301
+ Listen to events using `automator.on(event, callback)`:
302
+
303
+ * `ready` - Scheduler started
304
+ * `action` - Action executed
305
+ * `update` - Action added/updated/removed
306
+ * `error` - Error occurred
307
+ * `debug` - Debug information
308
+
309
+ ```js
310
+ automator.on('action', (event) => {
311
+ console.log('Action executed:', event.name);
312
+ console.log('Scheduled:', event.scheduledTime);
313
+ console.log('Actual:', event.actualTime);
314
+ });
315
+ ```
316
+
317
+ ### Storage Adapters
318
+
319
+ #### File Storage
320
+
321
+ ```js
322
+ const automator = new Automator({
323
+ storage: Automator.storage.file('./actions.json')
324
+ });
325
+ ```
326
+
327
+ #### Memory Storage
328
+
329
+ ```js
330
+ const automator = new Automator({
331
+ storage: Automator.storage.memory()
332
+ });
333
+ ```
334
+
335
+ #### Custom Storage
336
+
337
+ ```js
338
+ const automator = new Automator({
339
+ storage: {
340
+ load: function() {
341
+ // Return { actions: [...] }
342
+ },
343
+ save: function(state) {
344
+ // Save state
230
345
  }
231
- };
346
+ }
347
+ });
232
348
  ```
233
349
 
234
- ## Emitters ##
235
- The automator will emit the following events, use with:
350
+ ---
236
351
 
237
- ```javascript
238
- automator.on('emitterName', (payload)=> {
239
- console.log(payload);
240
- });
352
+ ## ๐Ÿ“Š Example: Sensor Reading Every Second
353
+
354
+ ```js
355
+ automator.addAction({
356
+ name: 'TempSensor',
357
+ cmd: 'readTemp',
358
+ date: null, // run immediately
359
+ payload: null,
360
+ unBuffered: false, // catch up if delayed
361
+ repeat: {
362
+ type: 'second',
363
+ interval: 1
364
+ }
365
+ });
241
366
  ```
242
367
 
243
- __debug__
244
- Emits debug notifications
368
+ If the system stalls:
369
+
370
+ * At 00:00:00 โ†’ reading #1
371
+ * Heavy load โ†’ no ticks for 5 seconds
372
+ * At 00:00:06 โ†’ automator triggers readings #2โ€“#6, advancing schedule
373
+
374
+ Your "60 readings per minute" pattern is preserved logically.
245
375
 
246
376
  ---
247
377
 
248
- __ready__
249
- Fires when the automator object is ready for use after being declared. The automator always fires exactly on the second so it may take up to 999 milliseconds for the automator to start. There may also be an async file read when you declare the automator.
378
+ ## ๐Ÿ•ฐ DST Behavior Examples
379
+
380
+ ### Fall Back (Repeated Hour)
381
+
382
+ 07:30 happens twice:
383
+
384
+ ```
385
+ 1) 07:30 (DST)
386
+ 2) 07:30 (Standard)
387
+ ```
388
+
389
+ User chooses:
390
+
391
+ * `dstPolicy: 'twice'` โ†’ run both
392
+ * `dstPolicy: 'once'` โ†’ run only the first instance
393
+
394
+ ### Spring Forward (Missing Hour)
395
+
396
+ 02:30 does not exist.
397
+
398
+ * Buffered โ†’ run as soon as possible after the jump
399
+ * Unbuffered โ†’ skip silently
250
400
 
251
401
  ---
252
402
 
253
- __error__
254
- Emits error messages
403
+ ## ๐Ÿงช Testing
404
+
405
+ ```bash
406
+ npm test
407
+ npm run test:coverage
408
+ ```
255
409
 
256
410
  ---
257
411
 
258
- __update__
259
- Fires when the action list is updated via updateAction() or addAction()
412
+ ## ๐Ÿ“ฆ Action Specification
413
+
414
+ ### Top-level action fields:
415
+
416
+ | Field | Description |
417
+ | ------------ | ------------------------------------------------- |
418
+ | `id` | Unique internal identifier (auto-generated) |
419
+ | `name` | User label (optional) |
420
+ | `cmd` | Name of registered function to execute |
421
+ | `payload` | Data passed to the command |
422
+ | `date` | Next scheduled run time (local `Date`) |
423
+ | `unBuffered` | Skip missed events (`true`) or catch up (`false`) |
424
+
425
+ ### Repeat block:
426
+
427
+ | Field | Description |
428
+ | ----------- | --------------------------------- |
429
+ | `type` | Recurrence unit |
430
+ | `interval` | Nth occurrence |
431
+ | `limit` | Number of times to run, or `null` |
432
+ | `endDate` | Max date, or `null` |
433
+ | `count` | Execution counter (internal) |
434
+ | `dstPolicy` | `'once'` or `'twice'` |
260
435
 
261
436
  ---
262
437
 
263
- __action__
264
- Returns a list of actions that were run in that second.
438
+ ## ๐ŸŽฏ Project Goals (v3)
265
439
 
440
+ * Deterministic behavior
441
+ * Rock-solid DST handling
442
+ * Predictable local-time recurrence
443
+ * Resilience to offline and delays
444
+ * Developer-friendly ergonomics
445
+ * Suitable for small devices
446
+ * Approachable but powerful API
447
+ * Long-term maintainability
266
448
 
267
- ## Advanced Use ##
449
+ ---
268
450
 
269
- You can use the automator to add actions to itself or to run millisecond interval events using the functions called from a primary action.
451
+ ## ๐Ÿ“ License
270
452
 
271
- __example #1__
453
+ MIT
272
454
 
273
- A primary action runs every hour which creates a secondary action that runs every minute for 5 minutes to read a sensor for the purpose of averaging the readings. This is also an example of a reason to use the limit option instead of the endDate option.
455
+ ---
274
456
 
275
- __example #2__
457
+ ## โค๏ธ Acknowledgments
276
458
 
277
- Same as #1, but this time your sensor has millisecond level speed and instead of creating a secondary action you have 2 primary actions, 1 that starts a setInterval() call using a sub-second interval and one that stops the interval some time in the future.
459
+ jw-automator v3 is a ground-up rethinking of the original jw-automator library, preserving the spirit while strengthening the foundations.
278
460
 
461
+ If you're building automation logic and want predictable, human-friendly scheduling that survives the real world โ€” **welcome.**