jw-automator 2.0.0 โ 3.0.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/CHANGELOG.md +76 -0
- package/README.md +366 -192
- package/docs/ARCHITECTURE.md +342 -0
- package/docs/MIGRATION.md +407 -0
- package/docs/QUICKSTART.md +350 -0
- package/examples/basic-example.js +135 -0
- package/examples/hello-world.js +40 -0
- package/examples/iot-sensor-example.js +149 -0
- package/index.js +7 -0
- package/package.json +53 -19
- package/src/Automator.js +429 -0
- package/src/core/CoreEngine.js +210 -0
- package/src/core/RecurrenceEngine.js +237 -0
- package/src/host/SchedulerHost.js +174 -0
- package/src/storage/FileStorage.js +59 -0
- package/src/storage/MemoryStorage.js +27 -0
- package/.actions.json +0 -1
- package/.jshintrc +0 -16
- package/.vscode/settings.json +0 -6
- package/LICENSE +0 -674
- package/automator.js +0 -696
- package/demo.js +0 -76
package/README.md
CHANGED
|
@@ -1,278 +1,452 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
5
|
+
**Human-friendly recurrence rules. Offline catch-up. DST-safe. Predictable. Extensible.**
|
|
45
6
|
|
|
46
7
|
---
|
|
47
8
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
1. name _string_: The action name
|
|
42
|
+
```bash
|
|
43
|
+
npm install jw-automator
|
|
44
|
+
```
|
|
78
45
|
|
|
79
|
-
|
|
46
|
+
### Basic Usage
|
|
80
47
|
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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**:
|
|
132
|
+
|
|
133
|
+
* **Spring forward:** missing hour handled via buffered/unBuffered rules
|
|
134
|
+
* **Fall back:** user chooses `dstPolicy: 'once' | 'twice'`
|
|
150
135
|
|
|
151
|
-
|
|
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.
|
|
136
|
+
This avoids cron's silent-but-surprising behaviors.
|
|
154
137
|
|
|
155
138
|
---
|
|
156
139
|
|
|
157
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
162
|
+
### 5. **Deterministic "Step Engine"**
|
|
163
|
+
|
|
164
|
+
The heart of jw-automator is a pure scheduling primitive:
|
|
165
|
+
|
|
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)
|
|
199
177
|
|
|
200
|
-
|
|
178
|
+
Because `step` is deterministic, you can:
|
|
201
179
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
#### `removeActionByID(id)`
|
|
261
|
+
Remove an action by ID.
|
|
262
|
+
|
|
263
|
+
#### `removeActionByName(name)`
|
|
264
|
+
Remove all actions with the given name.
|
|
265
|
+
|
|
266
|
+
#### `getActions()`
|
|
267
|
+
Get all actions (deep copy).
|
|
268
|
+
|
|
269
|
+
#### `getActionsByName(name)`
|
|
270
|
+
Get actions by name.
|
|
271
|
+
|
|
272
|
+
#### `getActionByID(id)`
|
|
273
|
+
Get a specific action by ID.
|
|
274
|
+
|
|
275
|
+
#### `getActionsInRange(startDate, endDate, callback)`
|
|
276
|
+
Simulate actions in a time range.
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
const events = automator.getActionsInRange(
|
|
280
|
+
new Date('2025-05-01'),
|
|
281
|
+
new Date('2025-05-07')
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
console.log(events); // Array of scheduled events
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `describeAction(id)`
|
|
288
|
+
Get a human-readable description of an action.
|
|
289
|
+
|
|
290
|
+
### Events
|
|
291
|
+
|
|
292
|
+
Listen to events using `automator.on(event, callback)`:
|
|
293
|
+
|
|
294
|
+
* `ready` - Scheduler started
|
|
295
|
+
* `action` - Action executed
|
|
296
|
+
* `update` - Action added/updated/removed
|
|
297
|
+
* `error` - Error occurred
|
|
298
|
+
* `debug` - Debug information
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
automator.on('action', (event) => {
|
|
302
|
+
console.log('Action executed:', event.name);
|
|
303
|
+
console.log('Scheduled:', event.scheduledTime);
|
|
304
|
+
console.log('Actual:', event.actualTime);
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Storage Adapters
|
|
309
|
+
|
|
310
|
+
#### File Storage
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
const automator = new Automator({
|
|
314
|
+
storage: Automator.storage.file('./actions.json')
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### Memory Storage
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
const automator = new Automator({
|
|
322
|
+
storage: Automator.storage.memory()
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Custom Storage
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
const automator = new Automator({
|
|
330
|
+
storage: {
|
|
331
|
+
load: function() {
|
|
332
|
+
// Return { actions: [...] }
|
|
333
|
+
},
|
|
334
|
+
save: function(state) {
|
|
335
|
+
// Save state
|
|
230
336
|
}
|
|
231
|
-
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
232
339
|
```
|
|
233
340
|
|
|
234
|
-
|
|
235
|
-
The automator will emit the following events, use with:
|
|
341
|
+
---
|
|
236
342
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
343
|
+
## ๐ Example: Sensor Reading Every Second
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
automator.addAction({
|
|
347
|
+
name: 'TempSensor',
|
|
348
|
+
cmd: 'readTemp',
|
|
349
|
+
date: null, // run immediately
|
|
350
|
+
payload: null,
|
|
351
|
+
unBuffered: false, // catch up if delayed
|
|
352
|
+
repeat: {
|
|
353
|
+
type: 'second',
|
|
354
|
+
interval: 1
|
|
355
|
+
}
|
|
356
|
+
});
|
|
241
357
|
```
|
|
242
358
|
|
|
243
|
-
|
|
244
|
-
|
|
359
|
+
If the system stalls:
|
|
360
|
+
|
|
361
|
+
* At 00:00:00 โ reading #1
|
|
362
|
+
* Heavy load โ no ticks for 5 seconds
|
|
363
|
+
* At 00:00:06 โ automator triggers readings #2โ#6, advancing schedule
|
|
364
|
+
|
|
365
|
+
Your "60 readings per minute" pattern is preserved logically.
|
|
245
366
|
|
|
246
367
|
---
|
|
247
368
|
|
|
248
|
-
|
|
249
|
-
|
|
369
|
+
## ๐ฐ DST Behavior Examples
|
|
370
|
+
|
|
371
|
+
### Fall Back (Repeated Hour)
|
|
372
|
+
|
|
373
|
+
07:30 happens twice:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
1) 07:30 (DST)
|
|
377
|
+
2) 07:30 (Standard)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
User chooses:
|
|
381
|
+
|
|
382
|
+
* `dstPolicy: 'twice'` โ run both
|
|
383
|
+
* `dstPolicy: 'once'` โ run only the first instance
|
|
384
|
+
|
|
385
|
+
### Spring Forward (Missing Hour)
|
|
386
|
+
|
|
387
|
+
02:30 does not exist.
|
|
388
|
+
|
|
389
|
+
* Buffered โ run as soon as possible after the jump
|
|
390
|
+
* Unbuffered โ skip silently
|
|
250
391
|
|
|
251
392
|
---
|
|
252
393
|
|
|
253
|
-
|
|
254
|
-
|
|
394
|
+
## ๐งช Testing
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
npm test
|
|
398
|
+
npm run test:coverage
|
|
399
|
+
```
|
|
255
400
|
|
|
256
401
|
---
|
|
257
402
|
|
|
258
|
-
|
|
259
|
-
|
|
403
|
+
## ๐ฆ Action Specification
|
|
404
|
+
|
|
405
|
+
### Top-level action fields:
|
|
406
|
+
|
|
407
|
+
| Field | Description |
|
|
408
|
+
| ------------ | ------------------------------------------------- |
|
|
409
|
+
| `id` | Unique internal identifier (auto-generated) |
|
|
410
|
+
| `name` | User label (optional) |
|
|
411
|
+
| `cmd` | Name of registered function to execute |
|
|
412
|
+
| `payload` | Data passed to the command |
|
|
413
|
+
| `date` | Next scheduled run time (local `Date`) |
|
|
414
|
+
| `unBuffered` | Skip missed events (`true`) or catch up (`false`) |
|
|
415
|
+
|
|
416
|
+
### Repeat block:
|
|
417
|
+
|
|
418
|
+
| Field | Description |
|
|
419
|
+
| ----------- | --------------------------------- |
|
|
420
|
+
| `type` | Recurrence unit |
|
|
421
|
+
| `interval` | Nth occurrence |
|
|
422
|
+
| `limit` | Number of times to run, or `null` |
|
|
423
|
+
| `endDate` | Max date, or `null` |
|
|
424
|
+
| `count` | Execution counter (internal) |
|
|
425
|
+
| `dstPolicy` | `'once'` or `'twice'` |
|
|
260
426
|
|
|
261
427
|
---
|
|
262
428
|
|
|
263
|
-
|
|
264
|
-
Returns a list of actions that were run in that second.
|
|
429
|
+
## ๐ฏ Project Goals (v3)
|
|
265
430
|
|
|
431
|
+
* Deterministic behavior
|
|
432
|
+
* Rock-solid DST handling
|
|
433
|
+
* Predictable local-time recurrence
|
|
434
|
+
* Resilience to offline and delays
|
|
435
|
+
* Developer-friendly ergonomics
|
|
436
|
+
* Suitable for small devices
|
|
437
|
+
* Approachable but powerful API
|
|
438
|
+
* Long-term maintainability
|
|
266
439
|
|
|
267
|
-
|
|
440
|
+
---
|
|
268
441
|
|
|
269
|
-
|
|
442
|
+
## ๐ License
|
|
270
443
|
|
|
271
|
-
|
|
444
|
+
MIT
|
|
272
445
|
|
|
273
|
-
|
|
446
|
+
---
|
|
274
447
|
|
|
275
|
-
|
|
448
|
+
## โค๏ธ Acknowledgments
|
|
276
449
|
|
|
277
|
-
|
|
450
|
+
jw-automator v3 is a ground-up rethinking of the original jw-automator library, preserving the spirit while strengthening the foundations.
|
|
278
451
|
|
|
452
|
+
If you're building automation logic and want predictable, human-friendly scheduling that survives the real world โ **welcome.**
|