signalk-automation-plugin 0.0.4 → 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/README.md +8 -8
- package/examples/automations.yaml +39 -37
- package/lib/engine.js +76 -4
- package/lib/runtime.js +57 -6
- package/lib/yaml.js +8 -1
- package/package.json +1 -1
- package/plugin/index.js +56 -9
- package/public/app.js +4 -1
- package/public/index.html +2 -2
package/README.md
CHANGED
|
@@ -36,34 +36,34 @@ Do not paste automations into the admin form. Runtime state is `~/.signalk/plugi
|
|
|
36
36
|
|
|
37
37
|
## Example
|
|
38
38
|
|
|
39
|
-
See `examples/automations.yaml
|
|
39
|
+
See `examples/automations.yaml` (fictional sample). Live YAML is whatever directory you set as `automationsDir` — usually a separate git. On/off and Verbose are plugin-data switches, not YAML keys.
|
|
40
40
|
|
|
41
41
|
```yaml
|
|
42
42
|
helpers:
|
|
43
|
-
|
|
43
|
+
dish_manual:
|
|
44
44
|
type: boolean
|
|
45
45
|
default: false
|
|
46
46
|
on_start: restore # survive reboot
|
|
47
|
-
latch_on_external_put: electrical.switches.
|
|
47
|
+
latch_on_external_put: electrical.switches.dish.state
|
|
48
48
|
|
|
49
49
|
automations:
|
|
50
|
-
- id:
|
|
50
|
+
- id: dish_standby
|
|
51
51
|
trigger:
|
|
52
52
|
- schedule: "0 4 * * *"
|
|
53
53
|
condition:
|
|
54
54
|
- zone: home_harbour
|
|
55
|
-
- helper:
|
|
55
|
+
- helper: dish_manual
|
|
56
56
|
is: false
|
|
57
57
|
action:
|
|
58
|
-
- put: electrical.switches.
|
|
58
|
+
- put: electrical.switches.dish.state
|
|
59
59
|
value: true
|
|
60
60
|
- delay: 15m
|
|
61
|
-
- put: electrical.switches.
|
|
61
|
+
- put: electrical.switches.dish.state
|
|
62
62
|
value: false
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
```yaml
|
|
66
|
-
- path: electrical.switches.
|
|
66
|
+
- path: electrical.switches.charger.voltage
|
|
67
67
|
round: 1 # nearest volt; 230.1 and 230.4 do not both fire
|
|
68
68
|
```
|
|
69
69
|
|
|
@@ -1,103 +1,105 @@
|
|
|
1
|
+
# Bundled sample only. Site YAML belongs in the git you set as automationsDir,
|
|
2
|
+
# not in this plugin.
|
|
3
|
+
|
|
1
4
|
helpers:
|
|
2
|
-
|
|
5
|
+
dish_manual:
|
|
3
6
|
type: boolean
|
|
4
|
-
name:
|
|
7
|
+
name: Dish manual
|
|
5
8
|
default: false
|
|
6
9
|
on_start: restore
|
|
7
|
-
latch_on_external_put: electrical.switches.
|
|
8
|
-
|
|
10
|
+
latch_on_external_put: electrical.switches.dish.state
|
|
11
|
+
trip_prep:
|
|
9
12
|
type: boolean
|
|
10
|
-
name:
|
|
13
|
+
name: Prepare to leave
|
|
11
14
|
default: false
|
|
12
15
|
on_start: restore
|
|
13
16
|
|
|
14
17
|
automations:
|
|
15
18
|
- id: shore_charge
|
|
16
|
-
alias:
|
|
19
|
+
alias: Shore charge
|
|
17
20
|
zones:
|
|
18
21
|
home_harbour:
|
|
19
|
-
name:
|
|
20
|
-
lat: 52.
|
|
21
|
-
lon:
|
|
22
|
+
name: Home harbour
|
|
23
|
+
lat: 52.1
|
|
24
|
+
lon: 4.9
|
|
22
25
|
radius: 150
|
|
23
26
|
trigger:
|
|
24
|
-
- path: sensors.presence.
|
|
27
|
+
- path: sensors.presence.shore
|
|
25
28
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
26
29
|
round: 0.01
|
|
27
|
-
- helper:
|
|
30
|
+
- helper: trip_prep
|
|
28
31
|
choose:
|
|
29
|
-
- alias:
|
|
32
|
+
- alias: charge underway
|
|
30
33
|
conditions:
|
|
31
|
-
- path: sensors.presence.
|
|
34
|
+
- path: sensors.presence.shore
|
|
32
35
|
is: true
|
|
33
36
|
- not:
|
|
34
37
|
zone: home_harbour
|
|
35
38
|
action:
|
|
36
|
-
- put: electrical.switches.
|
|
39
|
+
- put: electrical.switches.charger.state
|
|
37
40
|
value: 1
|
|
38
|
-
- alias:
|
|
41
|
+
- alias: charge to leave
|
|
39
42
|
conditions:
|
|
40
|
-
- path: sensors.presence.
|
|
43
|
+
- path: sensors.presence.shore
|
|
41
44
|
is: true
|
|
42
45
|
- zone: home_harbour
|
|
43
|
-
- helper:
|
|
46
|
+
- helper: trip_prep
|
|
44
47
|
is: true
|
|
45
48
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
46
49
|
below: 0.98
|
|
47
50
|
action:
|
|
48
|
-
- put: electrical.switches.
|
|
51
|
+
- put: electrical.switches.charger.state
|
|
49
52
|
value: 1
|
|
50
|
-
- alias:
|
|
53
|
+
- alias: leave charged
|
|
51
54
|
conditions:
|
|
52
|
-
- path: sensors.presence.
|
|
55
|
+
- path: sensors.presence.shore
|
|
53
56
|
is: true
|
|
54
57
|
- zone: home_harbour
|
|
55
|
-
- helper:
|
|
58
|
+
- helper: trip_prep
|
|
56
59
|
is: true
|
|
57
60
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
58
61
|
above: 0.97
|
|
59
62
|
action:
|
|
60
|
-
- put: electrical.switches.
|
|
63
|
+
- put: electrical.switches.charger.state
|
|
61
64
|
value: 0
|
|
62
|
-
- helper:
|
|
65
|
+
- helper: trip_prep
|
|
63
66
|
value: false
|
|
64
|
-
- alias:
|
|
67
|
+
- alias: home below 60%
|
|
65
68
|
conditions:
|
|
66
|
-
- path: sensors.presence.
|
|
69
|
+
- path: sensors.presence.shore
|
|
67
70
|
is: true
|
|
68
71
|
- zone: home_harbour
|
|
69
|
-
- helper:
|
|
72
|
+
- helper: trip_prep
|
|
70
73
|
is: false
|
|
71
74
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
72
75
|
below: 0.6
|
|
73
76
|
action:
|
|
74
|
-
- put: electrical.switches.
|
|
77
|
+
- put: electrical.switches.charger.state
|
|
75
78
|
value: 1
|
|
76
|
-
- alias:
|
|
79
|
+
- alias: home above 80%
|
|
77
80
|
conditions:
|
|
78
|
-
- path: sensors.presence.
|
|
81
|
+
- path: sensors.presence.shore
|
|
79
82
|
is: true
|
|
80
83
|
- zone: home_harbour
|
|
81
|
-
- helper:
|
|
84
|
+
- helper: trip_prep
|
|
82
85
|
is: false
|
|
83
86
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
84
87
|
above: 0.8
|
|
85
88
|
action:
|
|
86
|
-
- put: electrical.switches.
|
|
89
|
+
- put: electrical.switches.charger.state
|
|
87
90
|
value: 0
|
|
88
91
|
|
|
89
|
-
- id:
|
|
90
|
-
alias:
|
|
92
|
+
- id: dish_standby
|
|
93
|
+
alias: Dish standby update
|
|
91
94
|
trigger:
|
|
92
95
|
- schedule: "0 4 * * *"
|
|
93
96
|
condition:
|
|
94
|
-
# home_harbour is defined on shore_charge (Walstroom laadbeleid).
|
|
95
97
|
- zone: home_harbour
|
|
96
|
-
- helper:
|
|
98
|
+
- helper: dish_manual
|
|
97
99
|
is: false
|
|
98
100
|
action:
|
|
99
|
-
- put: electrical.switches.
|
|
101
|
+
- put: electrical.switches.dish.state
|
|
100
102
|
value: true
|
|
101
103
|
- delay: 15m
|
|
102
|
-
- put: electrical.switches.
|
|
104
|
+
- put: electrical.switches.dish.state
|
|
103
105
|
value: false
|
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/plugin/index.js
CHANGED
|
@@ -305,15 +305,36 @@ module.exports = function (app) {
|
|
|
305
305
|
})
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
function
|
|
309
|
-
return
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
308
|
+
function isMultipleSources (err) {
|
|
309
|
+
return /multiple sources for the given path/i.test(String((err && err.message) || err || ''))
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function putSources (p) {
|
|
313
|
+
const out = []
|
|
314
|
+
const seen = Object.create(null)
|
|
315
|
+
const add = (s) => {
|
|
316
|
+
if (!s) return
|
|
317
|
+
const id = String(s)
|
|
318
|
+
if (seen[id] || id.includes(PLUGIN_ID)) return
|
|
319
|
+
seen[id] = true
|
|
320
|
+
out.push(id)
|
|
321
|
+
}
|
|
322
|
+
const node = typeof app.getSelfPath === 'function' ? app.getSelfPath(p) : null
|
|
323
|
+
if (node && typeof node === 'object' && !Array.isArray(node)) {
|
|
324
|
+
add(node.$source)
|
|
325
|
+
const vals = node.values
|
|
326
|
+
if (vals && typeof vals === 'object') {
|
|
327
|
+
Object.keys(vals)
|
|
328
|
+
.sort((a, b) => Number(/^mqtt\b/i.test(b)) - Number(/^mqtt\b/i.test(a)))
|
|
329
|
+
.forEach(add)
|
|
316
330
|
}
|
|
331
|
+
}
|
|
332
|
+
if (runtime && runtime.values) add(runtime.values[p + '.$source'])
|
|
333
|
+
return out
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function callPutSelf (p, value, source) {
|
|
337
|
+
return new Promise((resolve, reject) => {
|
|
317
338
|
let settled = false
|
|
318
339
|
const finish = (err) => {
|
|
319
340
|
if (settled) return
|
|
@@ -332,7 +353,9 @@ module.exports = function (app) {
|
|
|
332
353
|
finish(null)
|
|
333
354
|
}
|
|
334
355
|
try {
|
|
335
|
-
const ret =
|
|
356
|
+
const ret = source
|
|
357
|
+
? app.putSelfPath(p, value, fromReply, source)
|
|
358
|
+
: app.putSelfPath(p, value, fromReply)
|
|
336
359
|
if (ret && typeof ret.then === 'function') {
|
|
337
360
|
ret.then(fromReply, finish)
|
|
338
361
|
}
|
|
@@ -342,6 +365,30 @@ module.exports = function (app) {
|
|
|
342
365
|
})
|
|
343
366
|
}
|
|
344
367
|
|
|
368
|
+
async function putPath (p, value) {
|
|
369
|
+
if (typeof app.putSelfPath !== 'function') {
|
|
370
|
+
app.handleMessage(PLUGIN_ID, {
|
|
371
|
+
updates: [{ values: [{ path: p, value }] }]
|
|
372
|
+
})
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
await callPutSelf(p, value)
|
|
377
|
+
} catch (err) {
|
|
378
|
+
if (!isMultipleSources(err)) throw err
|
|
379
|
+
let last = err
|
|
380
|
+
for (const source of putSources(p)) {
|
|
381
|
+
try {
|
|
382
|
+
await callPutSelf(p, value, source)
|
|
383
|
+
return
|
|
384
|
+
} catch (e) {
|
|
385
|
+
last = e
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
throw last
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
345
392
|
function notify (p, state, message) {
|
|
346
393
|
const npath = String(p).startsWith('notifications.') ? p : 'notifications.' + p
|
|
347
394
|
app.handleMessage(PLUGIN_ID, {
|
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>
|