signalk-automation-plugin 0.0.5 → 0.0.7
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 +68 -7
- package/lib/yaml.js +8 -1
- package/package.json +1 -1
- package/plugin/index.js +5 -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
|
@@ -21,6 +21,7 @@ class Runtime {
|
|
|
21
21
|
this.sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
22
22
|
this.putFn = opts.put
|
|
23
23
|
this.notifyFn = opts.notify
|
|
24
|
+
this.onHelper = opts.onHelper || null
|
|
24
25
|
this.log = opts.log || { debug () {}, error () {} }
|
|
25
26
|
|
|
26
27
|
this.doc = yaml.emptyDoc()
|
|
@@ -33,6 +34,7 @@ class Runtime {
|
|
|
33
34
|
this.lastCronMinute = ''
|
|
34
35
|
this.lastQuantized = {}
|
|
35
36
|
this.unsubscribes = []
|
|
37
|
+
this.inFlight = {}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
load () {
|
|
@@ -89,11 +91,20 @@ class Runtime {
|
|
|
89
91
|
to: coerced,
|
|
90
92
|
changed: true
|
|
91
93
|
})
|
|
94
|
+
this.emitHelperValue(id, coerced)
|
|
92
95
|
if (!opts || opts.kick !== false) {
|
|
93
96
|
await this.kickPath(helpers.helperPath(id), coerced)
|
|
94
97
|
}
|
|
95
98
|
}
|
|
96
|
-
|
|
99
|
+
const current = Object.prototype.hasOwnProperty.call(this.helperValues, id)
|
|
100
|
+
? this.helperValues[id]
|
|
101
|
+
: coerced
|
|
102
|
+
if (!engine.sameValue(current, coerced)) this.emitHelperValue(id, current)
|
|
103
|
+
return current
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
emitHelperValue (id, value) {
|
|
107
|
+
if (typeof this.onHelper === 'function') this.onHelper(id, value)
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
async resetHelper (id) {
|
|
@@ -162,16 +173,21 @@ class Runtime {
|
|
|
162
173
|
const key = d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate() + '-' + d.getHours() + '-' + d.getMinutes()
|
|
163
174
|
if (key === this.lastCronMinute) return
|
|
164
175
|
this.lastCronMinute = key
|
|
176
|
+
const runs = []
|
|
165
177
|
for (const auto of this.doc.automations) {
|
|
166
178
|
const schedules = (auto.trigger || []).filter((t) => t.schedule)
|
|
167
179
|
if (!schedules.length) continue
|
|
168
180
|
const hit = schedules.some((t) => engine.cronMatch(t.schedule, d))
|
|
169
|
-
if (hit)
|
|
181
|
+
if (hit) runs.push(this.maybeRun(auto, { schedule: schedules[0].schedule }))
|
|
170
182
|
}
|
|
183
|
+
await Promise.all(runs)
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
async maybeRun (auto, trigger) {
|
|
174
187
|
if (!this.isEnabled(auto)) return
|
|
188
|
+
const mode = auto.mode || 'parallel'
|
|
189
|
+
if (mode === 'single' && this.inFlight[auto.id]) return
|
|
190
|
+
if (mode === 'restart' && this.inFlight[auto.id]) this.inFlight[auto.id].abort()
|
|
175
191
|
const ctx = this.makeCtx(trigger)
|
|
176
192
|
const trig = await engine.evalListAll(auto.trigger || [], ctx)
|
|
177
193
|
const matched = engine.triggerListMatched(auto.trigger, trig.results)
|
|
@@ -199,9 +215,15 @@ class Runtime {
|
|
|
199
215
|
}
|
|
200
216
|
|
|
201
217
|
this.lastDebounce[auto.id] = this.now()
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
218
|
+
const abort = newAbort()
|
|
219
|
+
if (mode === 'single' || mode === 'restart') this.inFlight[auto.id] = abort
|
|
220
|
+
try {
|
|
221
|
+
const record = await engine.evaluateAutomation(auto, this.makeCtx(trigger, { abort }))
|
|
222
|
+
this.finishRun(auto, record, { forceTrace: false })
|
|
223
|
+
return record
|
|
224
|
+
} finally {
|
|
225
|
+
if (this.inFlight[auto.id] === abort) delete this.inFlight[auto.id]
|
|
226
|
+
}
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
keepTrace (auto, record, extra) {
|
|
@@ -237,11 +259,13 @@ class Runtime {
|
|
|
237
259
|
|
|
238
260
|
async evaluateOnStart () {
|
|
239
261
|
this.seedFromSelf()
|
|
262
|
+
const runs = []
|
|
240
263
|
for (const auto of this.doc.automations) {
|
|
241
264
|
if (!this.isEnabled(auto)) continue
|
|
242
265
|
if (this.scheduleOnly(auto)) continue
|
|
243
|
-
|
|
266
|
+
runs.push(this.maybeRun(auto, { start: true }))
|
|
244
267
|
}
|
|
268
|
+
await Promise.all(runs)
|
|
245
269
|
}
|
|
246
270
|
|
|
247
271
|
seedFromSelf () {
|
|
@@ -318,6 +342,24 @@ class Runtime {
|
|
|
318
342
|
|
|
319
343
|
makeCtx (trigger, extra) {
|
|
320
344
|
extra = extra || {}
|
|
345
|
+
const abort = extra.abort
|
|
346
|
+
const sleep = async (ms) => {
|
|
347
|
+
if (!abort) return this.sleep(ms)
|
|
348
|
+
try {
|
|
349
|
+
await Promise.race([
|
|
350
|
+
this.sleep(ms),
|
|
351
|
+
abort.promise
|
|
352
|
+
])
|
|
353
|
+
} catch (err) {
|
|
354
|
+
if (err && err.cancelled) throw err
|
|
355
|
+
throw err
|
|
356
|
+
}
|
|
357
|
+
if (abort.aborted) {
|
|
358
|
+
const err = new Error('superseded')
|
|
359
|
+
err.cancelled = true
|
|
360
|
+
throw err
|
|
361
|
+
}
|
|
362
|
+
}
|
|
321
363
|
return {
|
|
322
364
|
values: this.values,
|
|
323
365
|
zones: this.doc.zones,
|
|
@@ -326,10 +368,11 @@ class Runtime {
|
|
|
326
368
|
now: this.now(),
|
|
327
369
|
skipDelay: !!extra.skipDelay,
|
|
328
370
|
skipUnchanged: extra.skipUnchanged !== false,
|
|
371
|
+
abort: abort || null,
|
|
329
372
|
put: (p, value) => this.put(p, value),
|
|
330
373
|
notify: (p, state, message) => this.notify(p, state, message),
|
|
331
374
|
setHelper: (id, value) => this.setHelper(id, value, { kick: false }),
|
|
332
|
-
sleep
|
|
375
|
+
sleep,
|
|
333
376
|
runScript: engine.makeRunScript({
|
|
334
377
|
scriptsDir: this.scriptsDir,
|
|
335
378
|
timeoutMs: this.scriptTimeoutMs
|
|
@@ -446,6 +489,24 @@ class Runtime {
|
|
|
446
489
|
}
|
|
447
490
|
}
|
|
448
491
|
|
|
492
|
+
function newAbort () {
|
|
493
|
+
const abort = {
|
|
494
|
+
aborted: false,
|
|
495
|
+
abort () {
|
|
496
|
+
if (this.aborted) return
|
|
497
|
+
this.aborted = true
|
|
498
|
+
const err = new Error('superseded')
|
|
499
|
+
err.cancelled = true
|
|
500
|
+
this._reject(err)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
abort.promise = new Promise((_, reject) => {
|
|
504
|
+
abort._reject = reject
|
|
505
|
+
})
|
|
506
|
+
abort.promise.catch(() => {})
|
|
507
|
+
return abort
|
|
508
|
+
}
|
|
509
|
+
|
|
449
510
|
function asSwitch (v) {
|
|
450
511
|
if (v === true || v === 1 || v === '1' || v === 'on' || v === 'ON') return 1
|
|
451
512
|
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/plugin/index.js
CHANGED
|
@@ -426,6 +426,7 @@ module.exports = function (app) {
|
|
|
426
426
|
scriptTimeoutSeconds: options.scriptTimeoutSeconds,
|
|
427
427
|
put: putPath,
|
|
428
428
|
notify,
|
|
429
|
+
onHelper: (id, value) => emitHelper(id, value),
|
|
429
430
|
log: {
|
|
430
431
|
debug: app.debug ? app.debug.bind(app) : () => {},
|
|
431
432
|
error: app.error ? app.error.bind(app) : console.error,
|
|
@@ -461,7 +462,10 @@ module.exports = function (app) {
|
|
|
461
462
|
app.registerPutHandler('vessels.self', helpers.helperPath(id), (context, p, v, cb) => {
|
|
462
463
|
Promise.resolve(runtime.setHelper(id, v))
|
|
463
464
|
.then((next) => {
|
|
464
|
-
|
|
465
|
+
const live = Object.prototype.hasOwnProperty.call(runtime.helperValues, id)
|
|
466
|
+
? runtime.helperValues[id]
|
|
467
|
+
: next
|
|
468
|
+
emitHelper(id, live)
|
|
465
469
|
if (cb) cb({ state: 'COMPLETED' })
|
|
466
470
|
})
|
|
467
471
|
.catch((err) => {
|
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>
|