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 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: parts.some((p) => p.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 || 'skipped'
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.push(' ' + markPass(c) + ' ' + (c.label || ''))
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: 'skipped',
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 || []).length && !trig.pass) {
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' : 'skipped'
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
- if (this.values[p]) await this.setHelper(def.id, true)
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.evalList(auto.trigger || [], ctx)
170
- const matched = !(auto.trigger || []).length || trig.pass
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signalk-automation-plugin",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Home Assistant-like YAML automations for Signal K",
5
5
  "main": "plugin/index.js",
6
6
  "keywords": [
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 r = result || '—'
78
- return '<span class="badge ' + (result || '') + '">' + r + '</span>'
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 { return new Date(iso).toLocaleString() } catch (_) { return iso }
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=20260915log" />
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=20260915log"></script>
64
+ <script src="app.js?v=20260918ok"></script>
65
65
  </body>
66
66
  </html>