signalk-automation-plugin 0.0.9 → 0.0.11
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 +2 -1
- package/lib/engine.js +39 -0
- package/lib/runtime.js +49 -10
- package/lib/store.js +2 -1
- package/package.json +1 -1
- package/plugin/index.js +8 -0
package/README.md
CHANGED
|
@@ -31,8 +31,9 @@ Not Node-RED. Not a Home Assistant YAML clone. Actions are Signal K PUTs, helper
|
|
|
31
31
|
| Allowed scripts directory | Only relative `run.file` paths under this dir |
|
|
32
32
|
| Decision records to keep | Ring buffer per automation |
|
|
33
33
|
| Script timeout | Seconds |
|
|
34
|
+
| Startup silence | Seconds after start (default 60, 0 = off) during which path triggers do not fire; values are still recorded |
|
|
34
35
|
|
|
35
|
-
Do not paste automations into the admin form. Runtime state is `~/.signalk/plugin-config-data/signalk-automation-plugin/` (`state.json`, `live/`, `traces/`).
|
|
36
|
+
Do not paste automations into the admin form. Runtime state is `~/.signalk/plugin-config-data/signalk-automation-plugin/` (`state.json`, `live/`, `traces/`). `state.json` also keeps the last seen value of each trigger path (`pathValues`), so the same value republished after a restart does not re-trigger.
|
|
36
37
|
|
|
37
38
|
## Example
|
|
38
39
|
|
package/lib/engine.js
CHANGED
|
@@ -74,6 +74,23 @@ function evalClause (clause, ctx) {
|
|
|
74
74
|
clause: { not: true }
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
if (clause.triggered) {
|
|
78
|
+
const want = clause.triggered
|
|
79
|
+
const fired = ctx.trigger && ctx.trigger.path
|
|
80
|
+
let expected = null
|
|
81
|
+
if (want && typeof want === 'object') {
|
|
82
|
+
if (want.helper) expected = helperPath(want.helper)
|
|
83
|
+
else if (want.path) expected = want.path
|
|
84
|
+
} else if (typeof want === 'string') {
|
|
85
|
+
expected = want
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
pass: expected != null && fired === expected,
|
|
89
|
+
error: expected == null ? 'triggered needs helper or path' : null,
|
|
90
|
+
actual: fired || null,
|
|
91
|
+
expected
|
|
92
|
+
}
|
|
93
|
+
}
|
|
77
94
|
if (Array.isArray(clause.all)) {
|
|
78
95
|
const parts = clause.all.map((c) => withLabel(c, evalClause(c, ctx)))
|
|
79
96
|
return {
|
|
@@ -212,6 +229,12 @@ function fmtVal (value) {
|
|
|
212
229
|
function clauseLabel (clause, result) {
|
|
213
230
|
if (!clause || typeof clause !== 'object') return ''
|
|
214
231
|
if (clause.not) return 'not ' + clauseLabel(clause.not, result)
|
|
232
|
+
if (clause.triggered) {
|
|
233
|
+
const want = clause.triggered
|
|
234
|
+
if (want && want.helper) return 'triggered helper ' + want.helper
|
|
235
|
+
if (want && want.path) return 'triggered path ' + want.path
|
|
236
|
+
return 'triggered ' + fmtVal(want)
|
|
237
|
+
}
|
|
215
238
|
if (Array.isArray(clause.all)) return 'all'
|
|
216
239
|
if (Array.isArray(clause.any)) return 'any'
|
|
217
240
|
if (clause.zone) {
|
|
@@ -636,6 +659,20 @@ function sameValue (a, b) {
|
|
|
636
659
|
return false
|
|
637
660
|
}
|
|
638
661
|
|
|
662
|
+
function asSwitch (v) {
|
|
663
|
+
if (v === true || v === 1 || v === '1' || v === 'on' || v === 'ON') return 1
|
|
664
|
+
if (v === false || v === 0 || v === '0' || v === 'off' || v === 'OFF') return 0
|
|
665
|
+
return null
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function valueUnchanged (previous, next) {
|
|
669
|
+
if (previous === undefined) return false
|
|
670
|
+
if (sameValue(previous, next)) return true
|
|
671
|
+
const a = asSwitch(previous)
|
|
672
|
+
const b = asSwitch(next)
|
|
673
|
+
return a !== null && a === b
|
|
674
|
+
}
|
|
675
|
+
|
|
639
676
|
function makeRunScript (opts) {
|
|
640
677
|
return function (spec) {
|
|
641
678
|
return runScript({
|
|
@@ -664,6 +701,8 @@ module.exports = {
|
|
|
664
701
|
makeRunScript,
|
|
665
702
|
parseDuration,
|
|
666
703
|
sameValue,
|
|
704
|
+
asSwitch,
|
|
705
|
+
valueUnchanged,
|
|
667
706
|
clauseLabel,
|
|
668
707
|
triggerFiredLabel,
|
|
669
708
|
triggerListMatched,
|
package/lib/runtime.js
CHANGED
|
@@ -17,6 +17,9 @@ class Runtime {
|
|
|
17
17
|
this.scriptsDir = opts.scriptsDir
|
|
18
18
|
this.traceCount = opts.traceCount || 20
|
|
19
19
|
this.scriptTimeoutMs = (opts.scriptTimeoutSeconds || 30) * 1000
|
|
20
|
+
const silence = Number(opts.startupSilenceSeconds)
|
|
21
|
+
this.startupSilenceMs = (Number.isFinite(silence) && silence >= 0 ? silence : 0) * 1000
|
|
22
|
+
this.startedAt = null
|
|
20
23
|
this.now = opts.now || (() => Date.now())
|
|
21
24
|
this.sleep = opts.sleep || ((ms) => new Promise((r) => setTimeout(r, ms)))
|
|
22
25
|
this.putFn = opts.put
|
|
@@ -39,6 +42,8 @@ class Runtime {
|
|
|
39
42
|
|
|
40
43
|
load () {
|
|
41
44
|
this.state = store.loadState(this.dataDir)
|
|
45
|
+
if (!this.state.pathValues || typeof this.state.pathValues !== 'object') this.state.pathValues = {}
|
|
46
|
+
this.startedAt = this.now()
|
|
42
47
|
const live = store.liveDir(this.dataDir)
|
|
43
48
|
const from = fs.existsSync(live) ? live : this.automationsDir
|
|
44
49
|
this.doc = yaml.loadYamlDir(from)
|
|
@@ -145,12 +150,53 @@ class Runtime {
|
|
|
145
150
|
}
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
triggerPaths () {
|
|
154
|
+
const paths = new Set()
|
|
155
|
+
for (const auto of this.doc.automations) {
|
|
156
|
+
for (const t of auto.trigger || []) {
|
|
157
|
+
if (t && typeof t.path === 'string') paths.add(t.path)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return paths
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
inStartupSilence () {
|
|
164
|
+
if (this.startedAt == null || !(this.startupSilenceMs > 0)) return false
|
|
165
|
+
return this.now() - this.startedAt < this.startupSilenceMs
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
rememberPathValue (p, value) {
|
|
169
|
+
if (!this.triggerPaths().has(p)) return
|
|
170
|
+
const pv = this.state.pathValues || (this.state.pathValues = {})
|
|
171
|
+
const has = Object.prototype.hasOwnProperty.call(pv, p)
|
|
172
|
+
if (has && engine.sameValue(pv[p], value)) return
|
|
173
|
+
if (value === undefined) return
|
|
174
|
+
try {
|
|
175
|
+
pv[p] = JSON.parse(JSON.stringify(value))
|
|
176
|
+
} catch (_) {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
store.saveState(this.dataDir, this.state)
|
|
180
|
+
}
|
|
181
|
+
|
|
148
182
|
async handlePathChange (p, value, source) {
|
|
149
|
-
|
|
183
|
+
let previous = Object.prototype.hasOwnProperty.call(this.values, p)
|
|
150
184
|
? this.values[p]
|
|
151
185
|
: undefined
|
|
186
|
+
const inMemory = previous !== undefined
|
|
187
|
+
const pv = this.state.pathValues || {}
|
|
188
|
+
if (!inMemory && Object.prototype.hasOwnProperty.call(pv, p)) {
|
|
189
|
+
// Value remembered from before a restart.
|
|
190
|
+
previous = pv[p]
|
|
191
|
+
}
|
|
152
192
|
this.setPathValue(p, value, { $source: source })
|
|
153
|
-
|
|
193
|
+
this.rememberPathValue(p, value)
|
|
194
|
+
await this.applyLatches(p, source, inMemory ? previous : undefined)
|
|
195
|
+
if (engine.valueUnchanged(previous, value)) return
|
|
196
|
+
if (this.inStartupSilence()) {
|
|
197
|
+
this.log.debug('startup silence: not triggering on ' + p)
|
|
198
|
+
return
|
|
199
|
+
}
|
|
154
200
|
await this.kickPath(p, value)
|
|
155
201
|
}
|
|
156
202
|
|
|
@@ -159,8 +205,7 @@ class Runtime {
|
|
|
159
205
|
if (fromSelf) return
|
|
160
206
|
if (previous === undefined) return
|
|
161
207
|
const next = this.values[p]
|
|
162
|
-
if (engine.
|
|
163
|
-
if (asSwitch(previous) !== null && asSwitch(previous) === asSwitch(next)) return
|
|
208
|
+
if (engine.valueUnchanged(previous, next)) return
|
|
164
209
|
for (const def of Object.values(this.doc.helpers)) {
|
|
165
210
|
if (def.latch_on_external_put === p && def.type === 'boolean') {
|
|
166
211
|
await this.setHelper(def.id, true)
|
|
@@ -507,12 +552,6 @@ function newAbort () {
|
|
|
507
552
|
return abort
|
|
508
553
|
}
|
|
509
554
|
|
|
510
|
-
function asSwitch (v) {
|
|
511
|
-
if (v === true || v === 1 || v === '1' || v === 'on' || v === 'ON') return 1
|
|
512
|
-
if (v === false || v === 0 || v === '0' || v === 'off' || v === 'OFF') return 0
|
|
513
|
-
return null
|
|
514
|
-
}
|
|
515
|
-
|
|
516
555
|
function holdMs (auto) {
|
|
517
556
|
for (const t of auto.trigger || []) {
|
|
518
557
|
if (t.for) {
|
package/lib/store.js
CHANGED
package/package.json
CHANGED
package/plugin/index.js
CHANGED
|
@@ -84,6 +84,13 @@ module.exports = function (app) {
|
|
|
84
84
|
title: 'Script timeout (seconds)',
|
|
85
85
|
default: 30,
|
|
86
86
|
minimum: 1
|
|
87
|
+
},
|
|
88
|
+
startupSilence: {
|
|
89
|
+
type: 'number',
|
|
90
|
+
title: 'Startup silence (seconds)',
|
|
91
|
+
description: 'After start, path triggers do not fire for this many seconds (values are still recorded). 0 = off.',
|
|
92
|
+
default: 60,
|
|
93
|
+
minimum: 0
|
|
87
94
|
}
|
|
88
95
|
}
|
|
89
96
|
}
|
|
@@ -424,6 +431,7 @@ module.exports = function (app) {
|
|
|
424
431
|
scriptsDir: options.scriptsDir || '',
|
|
425
432
|
traceCount: options.traceCount,
|
|
426
433
|
scriptTimeoutSeconds: options.scriptTimeoutSeconds,
|
|
434
|
+
startupSilenceSeconds: options.startupSilence == null ? 60 : options.startupSilence,
|
|
427
435
|
put: putPath,
|
|
428
436
|
notify,
|
|
429
437
|
onHelper: (id, value) => emitHelper(id, value),
|