signalk-automation-plugin 0.0.5 → 0.0.6
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/lib/engine.js +76 -4
- package/lib/runtime.js +57 -6
- package/lib/yaml.js +8 -1
- package/package.json +1 -1
- package/public/app.js +4 -1
- package/public/index.html +2 -2
package/lib/engine.js
CHANGED
|
@@ -37,6 +37,12 @@ function addPathsFromAction (paths, action) {
|
|
|
37
37
|
if (!action || typeof action !== 'object') return
|
|
38
38
|
if (action.put) paths.add(action.put)
|
|
39
39
|
if (action.valueFrom) paths.add(action.valueFrom)
|
|
40
|
+
const gate = action.if
|
|
41
|
+
if (Array.isArray(gate)) {
|
|
42
|
+
for (const c of gate) addPathsFromClause(paths, c)
|
|
43
|
+
} else if (gate) {
|
|
44
|
+
addPathsFromClause(paths, gate)
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
function getByPath (values, p) {
|
|
@@ -306,6 +312,31 @@ function resultLabel (result) {
|
|
|
306
312
|
return result === 'failure' ? 'Failed' : 'OK'
|
|
307
313
|
}
|
|
308
314
|
|
|
315
|
+
function formatStream (text) {
|
|
316
|
+
const t = String(text == null ? '' : text).replace(/\s+$/, '')
|
|
317
|
+
if (!t) return '(empty)'
|
|
318
|
+
if (!t.includes('\n')) return t
|
|
319
|
+
return '\n' + t.split('\n').map((line) => ' ' + line).join('\n')
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function actionStderr (action) {
|
|
323
|
+
if (action && action.stderr) return String(action.stderr)
|
|
324
|
+
const err = action && action.error ? String(action.error) : ''
|
|
325
|
+
if (err.startsWith('stderr: ')) return err.slice(8)
|
|
326
|
+
return ''
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function appendRunActionLines (lines, action) {
|
|
330
|
+
const extra = (action.args && action.args.length) ? ' ' + action.args.join(' ') : ''
|
|
331
|
+
lines.push(' run ' + (action.file || '') + extra)
|
|
332
|
+
lines.push(' stdout: ' + formatStream(action.stdout))
|
|
333
|
+
lines.push(' stderr: ' + formatStream(actionStderr(action)))
|
|
334
|
+
const err = action.error ? String(action.error) : ''
|
|
335
|
+
if (err && !err.startsWith('stderr: ')) {
|
|
336
|
+
lines.push(' error: ' + err)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
309
340
|
function formatVerboseLog (auto, record) {
|
|
310
341
|
const name = (auto && (auto.alias || auto.id)) || (record && record.id) || 'automation'
|
|
311
342
|
const result = resultLabel(record.result)
|
|
@@ -327,6 +358,9 @@ function formatVerboseLog (auto, record) {
|
|
|
327
358
|
appendClauseLines(lines, c, ' ')
|
|
328
359
|
}
|
|
329
360
|
}
|
|
361
|
+
for (const a of record.actions || []) {
|
|
362
|
+
if (a && a.type === 'run') appendRunActionLines(lines, a)
|
|
363
|
+
}
|
|
330
364
|
return lines.join('\n')
|
|
331
365
|
}
|
|
332
366
|
|
|
@@ -475,6 +509,11 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
475
509
|
}
|
|
476
510
|
|
|
477
511
|
for (const action of actions) {
|
|
512
|
+
if (ctx.abort && ctx.abort.aborted) {
|
|
513
|
+
record.result = 'ok'
|
|
514
|
+
record.reason = 'superseded'
|
|
515
|
+
return finishRecord(auto, record)
|
|
516
|
+
}
|
|
478
517
|
const ran = await runAction(action, ctx)
|
|
479
518
|
record.actions.push(ran)
|
|
480
519
|
if (!ran.ok) {
|
|
@@ -482,6 +521,7 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
482
521
|
record.reason = ran.error
|
|
483
522
|
return finishRecord(auto, record)
|
|
484
523
|
}
|
|
524
|
+
if (ran.stop) break
|
|
485
525
|
}
|
|
486
526
|
record.result = 'ok'
|
|
487
527
|
return finishRecord(auto, record)
|
|
@@ -491,6 +531,12 @@ async function runAction (action, ctx) {
|
|
|
491
531
|
if (!action || typeof action !== 'object') {
|
|
492
532
|
return { ok: false, error: 'empty action' }
|
|
493
533
|
}
|
|
534
|
+
if (action.if) {
|
|
535
|
+
const gate = await evalListAll(Array.isArray(action.if) ? action.if : [action.if], ctx)
|
|
536
|
+
if (!gate.pass) {
|
|
537
|
+
return { ok: true, skipped: true, reason: 'if not met', if: gate.results }
|
|
538
|
+
}
|
|
539
|
+
}
|
|
494
540
|
if (action.put) {
|
|
495
541
|
try {
|
|
496
542
|
const value = Object.prototype.hasOwnProperty.call(action, 'valueFrom')
|
|
@@ -533,20 +579,46 @@ async function runAction (action, ctx) {
|
|
|
533
579
|
}
|
|
534
580
|
if (action.delay != null) {
|
|
535
581
|
const ms = parseDuration(action.delay)
|
|
536
|
-
if (ctx.skipDelay) return { ok: true, type: 'delay', ms, skipped: true }
|
|
537
|
-
|
|
582
|
+
if (ctx.skipDelay) return { ok: true, type: 'delay', ms, skipped: true, stop: true }
|
|
583
|
+
try {
|
|
584
|
+
await ctx.sleep(ms)
|
|
585
|
+
} catch (err) {
|
|
586
|
+
if (err && err.cancelled) {
|
|
587
|
+
return { ok: true, type: 'delay', ms, cancelled: true, stop: true }
|
|
588
|
+
}
|
|
589
|
+
throw err
|
|
590
|
+
}
|
|
591
|
+
if (ctx.abort && ctx.abort.aborted) {
|
|
592
|
+
return { ok: true, type: 'delay', ms, cancelled: true, stop: true }
|
|
593
|
+
}
|
|
538
594
|
return { ok: true, type: 'delay', ms }
|
|
539
595
|
}
|
|
540
596
|
if (action.run) {
|
|
541
597
|
const spec = typeof action.run === 'string' ? { file: action.run } : action.run
|
|
542
598
|
const ran = await ctx.runScript(spec)
|
|
543
599
|
if (!ran.ok) {
|
|
544
|
-
return {
|
|
600
|
+
return {
|
|
601
|
+
ok: false,
|
|
602
|
+
type: 'run',
|
|
603
|
+
file: spec.file,
|
|
604
|
+
args: spec.args || [],
|
|
605
|
+
error: ran.error,
|
|
606
|
+
stdout: ran.stdout,
|
|
607
|
+
stderr: ran.stderr || ''
|
|
608
|
+
}
|
|
545
609
|
}
|
|
546
610
|
if (action.as) {
|
|
547
611
|
ctx.setHelper(action.as, ran.value)
|
|
548
612
|
}
|
|
549
|
-
return {
|
|
613
|
+
return {
|
|
614
|
+
ok: true,
|
|
615
|
+
type: 'run',
|
|
616
|
+
file: spec.file,
|
|
617
|
+
args: spec.args || [],
|
|
618
|
+
value: ran.value,
|
|
619
|
+
stdout: ran.stdout,
|
|
620
|
+
stderr: ran.stderr || ''
|
|
621
|
+
}
|
|
550
622
|
}
|
|
551
623
|
return { ok: false, error: 'unknown action' }
|
|
552
624
|
}
|
package/lib/runtime.js
CHANGED
|
@@ -33,6 +33,7 @@ class Runtime {
|
|
|
33
33
|
this.lastCronMinute = ''
|
|
34
34
|
this.lastQuantized = {}
|
|
35
35
|
this.unsubscribes = []
|
|
36
|
+
this.inFlight = {}
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
load () {
|
|
@@ -162,16 +163,21 @@ class Runtime {
|
|
|
162
163
|
const key = d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate() + '-' + d.getHours() + '-' + d.getMinutes()
|
|
163
164
|
if (key === this.lastCronMinute) return
|
|
164
165
|
this.lastCronMinute = key
|
|
166
|
+
const runs = []
|
|
165
167
|
for (const auto of this.doc.automations) {
|
|
166
168
|
const schedules = (auto.trigger || []).filter((t) => t.schedule)
|
|
167
169
|
if (!schedules.length) continue
|
|
168
170
|
const hit = schedules.some((t) => engine.cronMatch(t.schedule, d))
|
|
169
|
-
if (hit)
|
|
171
|
+
if (hit) runs.push(this.maybeRun(auto, { schedule: schedules[0].schedule }))
|
|
170
172
|
}
|
|
173
|
+
await Promise.all(runs)
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
async maybeRun (auto, trigger) {
|
|
174
177
|
if (!this.isEnabled(auto)) return
|
|
178
|
+
const mode = auto.mode || 'parallel'
|
|
179
|
+
if (mode === 'single' && this.inFlight[auto.id]) return
|
|
180
|
+
if (mode === 'restart' && this.inFlight[auto.id]) this.inFlight[auto.id].abort()
|
|
175
181
|
const ctx = this.makeCtx(trigger)
|
|
176
182
|
const trig = await engine.evalListAll(auto.trigger || [], ctx)
|
|
177
183
|
const matched = engine.triggerListMatched(auto.trigger, trig.results)
|
|
@@ -199,9 +205,15 @@ class Runtime {
|
|
|
199
205
|
}
|
|
200
206
|
|
|
201
207
|
this.lastDebounce[auto.id] = this.now()
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
208
|
+
const abort = newAbort()
|
|
209
|
+
if (mode === 'single' || mode === 'restart') this.inFlight[auto.id] = abort
|
|
210
|
+
try {
|
|
211
|
+
const record = await engine.evaluateAutomation(auto, this.makeCtx(trigger, { abort }))
|
|
212
|
+
this.finishRun(auto, record, { forceTrace: false })
|
|
213
|
+
return record
|
|
214
|
+
} finally {
|
|
215
|
+
if (this.inFlight[auto.id] === abort) delete this.inFlight[auto.id]
|
|
216
|
+
}
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
keepTrace (auto, record, extra) {
|
|
@@ -237,11 +249,13 @@ class Runtime {
|
|
|
237
249
|
|
|
238
250
|
async evaluateOnStart () {
|
|
239
251
|
this.seedFromSelf()
|
|
252
|
+
const runs = []
|
|
240
253
|
for (const auto of this.doc.automations) {
|
|
241
254
|
if (!this.isEnabled(auto)) continue
|
|
242
255
|
if (this.scheduleOnly(auto)) continue
|
|
243
|
-
|
|
256
|
+
runs.push(this.maybeRun(auto, { start: true }))
|
|
244
257
|
}
|
|
258
|
+
await Promise.all(runs)
|
|
245
259
|
}
|
|
246
260
|
|
|
247
261
|
seedFromSelf () {
|
|
@@ -318,6 +332,24 @@ class Runtime {
|
|
|
318
332
|
|
|
319
333
|
makeCtx (trigger, extra) {
|
|
320
334
|
extra = extra || {}
|
|
335
|
+
const abort = extra.abort
|
|
336
|
+
const sleep = async (ms) => {
|
|
337
|
+
if (!abort) return this.sleep(ms)
|
|
338
|
+
try {
|
|
339
|
+
await Promise.race([
|
|
340
|
+
this.sleep(ms),
|
|
341
|
+
abort.promise
|
|
342
|
+
])
|
|
343
|
+
} catch (err) {
|
|
344
|
+
if (err && err.cancelled) throw err
|
|
345
|
+
throw err
|
|
346
|
+
}
|
|
347
|
+
if (abort.aborted) {
|
|
348
|
+
const err = new Error('superseded')
|
|
349
|
+
err.cancelled = true
|
|
350
|
+
throw err
|
|
351
|
+
}
|
|
352
|
+
}
|
|
321
353
|
return {
|
|
322
354
|
values: this.values,
|
|
323
355
|
zones: this.doc.zones,
|
|
@@ -326,10 +358,11 @@ class Runtime {
|
|
|
326
358
|
now: this.now(),
|
|
327
359
|
skipDelay: !!extra.skipDelay,
|
|
328
360
|
skipUnchanged: extra.skipUnchanged !== false,
|
|
361
|
+
abort: abort || null,
|
|
329
362
|
put: (p, value) => this.put(p, value),
|
|
330
363
|
notify: (p, state, message) => this.notify(p, state, message),
|
|
331
364
|
setHelper: (id, value) => this.setHelper(id, value, { kick: false }),
|
|
332
|
-
sleep
|
|
365
|
+
sleep,
|
|
333
366
|
runScript: engine.makeRunScript({
|
|
334
367
|
scriptsDir: this.scriptsDir,
|
|
335
368
|
timeoutMs: this.scriptTimeoutMs
|
|
@@ -446,6 +479,24 @@ class Runtime {
|
|
|
446
479
|
}
|
|
447
480
|
}
|
|
448
481
|
|
|
482
|
+
function newAbort () {
|
|
483
|
+
const abort = {
|
|
484
|
+
aborted: false,
|
|
485
|
+
abort () {
|
|
486
|
+
if (this.aborted) return
|
|
487
|
+
this.aborted = true
|
|
488
|
+
const err = new Error('superseded')
|
|
489
|
+
err.cancelled = true
|
|
490
|
+
this._reject(err)
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
abort.promise = new Promise((_, reject) => {
|
|
494
|
+
abort._reject = reject
|
|
495
|
+
})
|
|
496
|
+
abort.promise.catch(() => {})
|
|
497
|
+
return abort
|
|
498
|
+
}
|
|
499
|
+
|
|
449
500
|
function asSwitch (v) {
|
|
450
501
|
if (v === true || v === 1 || v === '1' || v === 'on' || v === 'ON') return 1
|
|
451
502
|
if (v === false || v === 0 || v === '0' || v === 'off' || v === 'OFF') return 0
|
package/lib/yaml.js
CHANGED
|
@@ -172,8 +172,15 @@ function mergeAutomations (result, list, file) {
|
|
|
172
172
|
action: asArray(raw.action),
|
|
173
173
|
choose: Array.isArray(raw.choose) ? raw.choose : [],
|
|
174
174
|
debounceMs: raw.debounceMs || 0,
|
|
175
|
-
edge: raw.edge || 'every'
|
|
175
|
+
edge: raw.edge || 'every',
|
|
176
|
+
mode: raw.mode || 'parallel'
|
|
176
177
|
})
|
|
178
|
+
if (raw.mode && ['parallel', 'restart', 'single'].indexOf(raw.mode) === -1) {
|
|
179
|
+
result.errors.push({
|
|
180
|
+
file,
|
|
181
|
+
message: 'Automation ' + raw.id + ': mode must be parallel, restart, or single'
|
|
182
|
+
})
|
|
183
|
+
}
|
|
177
184
|
for (const t of asArray(raw.trigger)) {
|
|
178
185
|
if (!t || t.round == null) continue
|
|
179
186
|
const n = Number(t.round)
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -98,7 +98,10 @@ function fmtActions (t) {
|
|
|
98
98
|
if (a.type === 'helper') return 'helper ' + a.helper + ' = ' + fmtVal(a.value)
|
|
99
99
|
if (a.type === 'notify') return 'notify ' + (a.path || '') + ' ' + (a.state || '')
|
|
100
100
|
if (a.type === 'delay') return a.skipped ? 'delay skipped (' + a.ms + ' ms)' : 'delay ' + a.ms + ' ms'
|
|
101
|
-
if (a.type === 'run')
|
|
101
|
+
if (a.type === 'run') {
|
|
102
|
+
const extra = (a.args && a.args.length) ? ' ' + a.args.join(' ') : ''
|
|
103
|
+
return 'run ' + (a.file || '') + extra
|
|
104
|
+
}
|
|
102
105
|
return a.type || 'action'
|
|
103
106
|
}).join('\n')
|
|
104
107
|
}
|
package/public/index.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Signal K Automations</title>
|
|
7
7
|
<link rel="icon" href="icon.png" />
|
|
8
|
-
<link rel="stylesheet" href="style.css?v=
|
|
8
|
+
<link rel="stylesheet" href="style.css?v=20260918args" />
|
|
9
9
|
</head>
|
|
10
10
|
<body>
|
|
11
11
|
<header>
|
|
@@ -61,6 +61,6 @@
|
|
|
61
61
|
</div>
|
|
62
62
|
</section>
|
|
63
63
|
</main>
|
|
64
|
-
<script src="app.js?v=
|
|
64
|
+
<script src="app.js?v=20260918args"></script>
|
|
65
65
|
</body>
|
|
66
66
|
</html>
|