signalk-automation-plugin 0.0.2 → 0.0.4
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 +1 -1
- package/lib/engine.js +44 -10
- package/lib/runtime.js +18 -5
- package/package.json +1 -1
- package/public/app.js +22 -3
- package/public/index.html +2 -2
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Not Node-RED. Not a Home Assistant YAML clone. Actions are Signal K PUTs, helper
|
|
|
11
11
|
- `restore` — survive reboot (save in plugin data, restore on start)
|
|
12
12
|
- `default` — set `default` on every start
|
|
13
13
|
- `none` — do nothing on start
|
|
14
|
-
- Path triggers, `choose`, zones, cron `schedule`
|
|
14
|
+
- Path triggers, `choose`, zones, cron `schedule` (trigger list is **any**; conditions are **all**)
|
|
15
15
|
- Trigger `round:` quantizes numbers (and lat/lon) so 230.1 V and 230.4 V do not both fire
|
|
16
16
|
- Actions: `put`, `helper`, `notify`, `delay`, `run`
|
|
17
17
|
- `run:` executes a file under configured `scriptsDir`. **stdout is the value; any stderr is fail**
|
package/lib/engine.js
CHANGED
|
@@ -69,7 +69,7 @@ function evalClause (clause, ctx) {
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
if (Array.isArray(clause.all)) {
|
|
72
|
-
const parts = clause.all.map((c) => evalClause(c, ctx))
|
|
72
|
+
const parts = clause.all.map((c) => withLabel(c, evalClause(c, ctx)))
|
|
73
73
|
return {
|
|
74
74
|
pass: parts.every((p) => p.pass),
|
|
75
75
|
error: parts.find((p) => p.error)?.error || null,
|
|
@@ -78,10 +78,11 @@ function evalClause (clause, ctx) {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
if (Array.isArray(clause.any)) {
|
|
81
|
-
const parts = clause.any.map((c) => evalClause(c, ctx))
|
|
81
|
+
const parts = clause.any.map((c) => withLabel(c, evalClause(c, ctx)))
|
|
82
|
+
const pass = parts.some((p) => p.pass)
|
|
82
83
|
return {
|
|
83
|
-
pass
|
|
84
|
-
error: parts.find((p) => p.error)?.error || null,
|
|
84
|
+
pass,
|
|
85
|
+
error: pass ? null : (parts.find((p) => p.error)?.error || null),
|
|
85
86
|
actual: parts,
|
|
86
87
|
clause: { any: true }
|
|
87
88
|
}
|
|
@@ -131,6 +132,18 @@ function evalClause (clause, ctx) {
|
|
|
131
132
|
return { pass: false, error: 'clause needs path, helper, zone, or run', actual: null }
|
|
132
133
|
}
|
|
133
134
|
const actual = getByPath(ctx.values, path)
|
|
135
|
+
const needsValue = ['is', 'equals', 'above', 'below', 'to'].some((k) =>
|
|
136
|
+
Object.prototype.hasOwnProperty.call(clause, k)
|
|
137
|
+
)
|
|
138
|
+
if (needsValue && (actual === undefined || actual === null)) {
|
|
139
|
+
return {
|
|
140
|
+
pass: false,
|
|
141
|
+
error: 'missing',
|
|
142
|
+
actual,
|
|
143
|
+
path,
|
|
144
|
+
expected: expectedOf(clause)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
134
147
|
let pass = false
|
|
135
148
|
if (Object.prototype.hasOwnProperty.call(clause, 'is')) {
|
|
136
149
|
pass = actual === clause.is
|
|
@@ -228,10 +241,20 @@ function clauseLabel (clause, result) {
|
|
|
228
241
|
|
|
229
242
|
function markPass (r) {
|
|
230
243
|
if (!r) return '✗'
|
|
231
|
-
if (r.error) return '✗'
|
|
244
|
+
if (r.error && !r.pass) return '✗'
|
|
232
245
|
return r.pass ? '✓' : '✗'
|
|
233
246
|
}
|
|
234
247
|
|
|
248
|
+
function appendClauseLines (lines, r, indent) {
|
|
249
|
+
if (!r) return
|
|
250
|
+
lines.push(indent + markPass(r) + ' ' + (r.label || ''))
|
|
251
|
+
const inner = r.clause && (r.clause.any || r.clause.all)
|
|
252
|
+
if (!inner || !Array.isArray(r.actual)) return
|
|
253
|
+
for (const part of r.actual) {
|
|
254
|
+
appendClauseLines(lines, part, indent + ' ')
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
235
258
|
function withLabel (clause, result) {
|
|
236
259
|
const row = result && typeof result === 'object' ? result : { pass: false }
|
|
237
260
|
row.clause = clause
|
|
@@ -255,6 +278,11 @@ function isBarePathClause (clause) {
|
|
|
255
278
|
)
|
|
256
279
|
}
|
|
257
280
|
|
|
281
|
+
function triggerListMatched (list, results) {
|
|
282
|
+
if (!(list || []).length) return true
|
|
283
|
+
return (results || []).some((r) => r.pass && !r.error)
|
|
284
|
+
}
|
|
285
|
+
|
|
258
286
|
async function evalList (list, ctx) {
|
|
259
287
|
const results = []
|
|
260
288
|
for (const clause of list || []) {
|
|
@@ -274,9 +302,13 @@ async function evalListAll (list, ctx) {
|
|
|
274
302
|
return { pass, results }
|
|
275
303
|
}
|
|
276
304
|
|
|
305
|
+
function resultLabel (result) {
|
|
306
|
+
return result === 'failure' ? 'Failed' : 'OK'
|
|
307
|
+
}
|
|
308
|
+
|
|
277
309
|
function formatVerboseLog (auto, record) {
|
|
278
310
|
const name = (auto && (auto.alias || auto.id)) || (record && record.id) || 'automation'
|
|
279
|
-
const result = record.result
|
|
311
|
+
const result = resultLabel(record.result)
|
|
280
312
|
const head = name + ' → ' + result + (record.reason ? ' (' + record.reason + ')' : '')
|
|
281
313
|
const lines = [head]
|
|
282
314
|
if (record.firedBy) lines.push(' trigger: ' + record.firedBy)
|
|
@@ -292,7 +324,7 @@ function formatVerboseLog (auto, record) {
|
|
|
292
324
|
for (const b of record.branches || []) {
|
|
293
325
|
lines.push(' ' + (b.picked ? '→ ' : ' ') + b.alias + (b.picked ? '' : ''))
|
|
294
326
|
for (const c of b.conditions || []) {
|
|
295
|
-
lines
|
|
327
|
+
appendClauseLines(lines, c, ' ')
|
|
296
328
|
}
|
|
297
329
|
}
|
|
298
330
|
return lines.join('\n')
|
|
@@ -387,7 +419,7 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
387
419
|
const record = {
|
|
388
420
|
ts: new Date(ctx.now || Date.now()).toISOString(),
|
|
389
421
|
id: auto.id,
|
|
390
|
-
result: '
|
|
422
|
+
result: 'ok',
|
|
391
423
|
trigger: ctx.trigger || null,
|
|
392
424
|
firedBy: triggerFiredLabel(ctx.trigger),
|
|
393
425
|
conditions: [],
|
|
@@ -402,7 +434,7 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
402
434
|
|
|
403
435
|
const trig = await evalListAll(auto.trigger || [], ctx)
|
|
404
436
|
record.triggerResults = trig.results
|
|
405
|
-
if ((auto.trigger
|
|
437
|
+
if (!triggerListMatched(auto.trigger, trig.results)) {
|
|
406
438
|
record.reason = 'trigger not matched'
|
|
407
439
|
return finishRecord(auto, record)
|
|
408
440
|
}
|
|
@@ -411,7 +443,7 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
411
443
|
record.conditions = cond.results
|
|
412
444
|
if ((auto.condition || []).length && !cond.pass) {
|
|
413
445
|
const failed = cond.results.find((r) => r.error)
|
|
414
|
-
record.result = failed ? 'failure' : '
|
|
446
|
+
record.result = failed ? 'failure' : 'ok'
|
|
415
447
|
record.reason = failed ? failed.error : 'condition not met'
|
|
416
448
|
return finishRecord(auto, record)
|
|
417
449
|
}
|
|
@@ -436,6 +468,7 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
436
468
|
record.branches.push(row)
|
|
437
469
|
}
|
|
438
470
|
if (!picked) {
|
|
471
|
+
record.result = 'ok'
|
|
439
472
|
record.reason = 'no matching choose branch'
|
|
440
473
|
return finishRecord(auto, record)
|
|
441
474
|
}
|
|
@@ -561,5 +594,6 @@ module.exports = {
|
|
|
561
594
|
sameValue,
|
|
562
595
|
clauseLabel,
|
|
563
596
|
triggerFiredLabel,
|
|
597
|
+
triggerListMatched,
|
|
564
598
|
formatVerboseLog
|
|
565
599
|
}
|
package/lib/runtime.js
CHANGED
|
@@ -135,17 +135,24 @@ class Runtime {
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
async handlePathChange (p, value, source) {
|
|
138
|
+
const previous = Object.prototype.hasOwnProperty.call(this.values, p)
|
|
139
|
+
? this.values[p]
|
|
140
|
+
: undefined
|
|
138
141
|
this.setPathValue(p, value, { $source: source })
|
|
139
|
-
await this.applyLatches(p, source)
|
|
142
|
+
await this.applyLatches(p, source, previous)
|
|
140
143
|
await this.kickPath(p, value)
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
async applyLatches (p, source) {
|
|
146
|
+
async applyLatches (p, source, previous) {
|
|
144
147
|
const fromSelf = source && String(source).includes(this.pluginId)
|
|
145
148
|
if (fromSelf) return
|
|
149
|
+
if (previous === undefined) return
|
|
150
|
+
const next = this.values[p]
|
|
151
|
+
if (engine.sameValue(previous, next)) return
|
|
152
|
+
if (asSwitch(previous) !== null && asSwitch(previous) === asSwitch(next)) return
|
|
146
153
|
for (const def of Object.values(this.doc.helpers)) {
|
|
147
154
|
if (def.latch_on_external_put === p && def.type === 'boolean') {
|
|
148
|
-
|
|
155
|
+
await this.setHelper(def.id, true)
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
}
|
|
@@ -166,8 +173,8 @@ class Runtime {
|
|
|
166
173
|
async maybeRun (auto, trigger) {
|
|
167
174
|
if (!this.isEnabled(auto)) return
|
|
168
175
|
const ctx = this.makeCtx(trigger)
|
|
169
|
-
const trig = await engine.
|
|
170
|
-
const matched =
|
|
176
|
+
const trig = await engine.evalListAll(auto.trigger || [], ctx)
|
|
177
|
+
const matched = engine.triggerListMatched(auto.trigger, trig.results)
|
|
171
178
|
|
|
172
179
|
if (!matched) {
|
|
173
180
|
this.lastTruthy[auto.id] = false
|
|
@@ -439,6 +446,12 @@ class Runtime {
|
|
|
439
446
|
}
|
|
440
447
|
}
|
|
441
448
|
|
|
449
|
+
function asSwitch (v) {
|
|
450
|
+
if (v === true || v === 1 || v === '1' || v === 'on' || v === 'ON') return 1
|
|
451
|
+
if (v === false || v === 0 || v === '0' || v === 'off' || v === 'OFF') return 0
|
|
452
|
+
return null
|
|
453
|
+
}
|
|
454
|
+
|
|
442
455
|
function holdMs (auto) {
|
|
443
456
|
for (const t of auto.trigger || []) {
|
|
444
457
|
if (t.for) {
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -73,9 +73,15 @@ async function checkLogin () {
|
|
|
73
73
|
|
|
74
74
|
function el (id) { return document.getElementById(id) }
|
|
75
75
|
|
|
76
|
+
function displayResult (result) {
|
|
77
|
+
if (!result) return '—'
|
|
78
|
+
if (result === 'failure') return 'Failed'
|
|
79
|
+
return 'OK'
|
|
80
|
+
}
|
|
81
|
+
|
|
76
82
|
function badge (result) {
|
|
77
|
-
const
|
|
78
|
-
return '<span class="badge ' +
|
|
83
|
+
const klass = !result ? '' : result === 'failure' ? 'failure' : 'ok'
|
|
84
|
+
return '<span class="badge ' + klass + '">' + displayResult(result) + '</span>'
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
function fmtVal (v) {
|
|
@@ -135,7 +141,20 @@ function fmtRunRecord (t) {
|
|
|
135
141
|
|
|
136
142
|
function fmtTime (iso) {
|
|
137
143
|
if (!iso) return ''
|
|
138
|
-
try {
|
|
144
|
+
try {
|
|
145
|
+
const d = new Date(iso)
|
|
146
|
+
if (Number.isNaN(d.getTime())) return iso
|
|
147
|
+
return new Intl.DateTimeFormat('nl-NL', {
|
|
148
|
+
timeZone: 'Europe/Amsterdam',
|
|
149
|
+
year: 'numeric',
|
|
150
|
+
month: '2-digit',
|
|
151
|
+
day: '2-digit',
|
|
152
|
+
hour: '2-digit',
|
|
153
|
+
minute: '2-digit',
|
|
154
|
+
second: '2-digit',
|
|
155
|
+
hourCycle: 'h23'
|
|
156
|
+
}).format(d)
|
|
157
|
+
} catch (_) { return iso }
|
|
139
158
|
}
|
|
140
159
|
|
|
141
160
|
function escapeHtml (s) {
|
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=20260918ok" />
|
|
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=20260918ok"></script>
|
|
65
65
|
</body>
|
|
66
66
|
</html>
|