signalk-automation-plugin 0.0.1 → 0.0.3
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 +17 -9
- package/examples/automations.yaml +49 -44
- package/lib/engine.js +240 -21
- package/lib/geo.js +17 -9
- package/lib/git.js +33 -1
- package/lib/runtime.js +217 -23
- package/lib/store.js +42 -1
- package/lib/yaml.js +85 -3
- package/package.json +2 -1
- package/plugin/index.js +205 -52
- package/public/app.js +504 -73
- package/public/icon.png +0 -0
- package/public/index.html +48 -21
- package/public/style.css +195 -20
package/README.md
CHANGED
|
@@ -11,7 +11,8 @@ 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
|
+
- Trigger `round:` quantizes numbers (and lat/lon) so 230.1 V and 230.4 V do not both fire
|
|
15
16
|
- Actions: `put`, `helper`, `notify`, `delay`, `run`
|
|
16
17
|
- `run:` executes a file under configured `scriptsDir`. **stdout is the value; any stderr is fail**
|
|
17
18
|
- Webapp: per-automation on/off, last result, last N decision records, live YAML, activate another git commit
|
|
@@ -35,7 +36,7 @@ Do not paste automations into the admin form. Runtime state is `~/.signalk/plugi
|
|
|
35
36
|
|
|
36
37
|
## Example
|
|
37
38
|
|
|
38
|
-
See `examples/automations.yaml`.
|
|
39
|
+
See `examples/automations.yaml`. Boatnet live YAML is `git@github.com:htool/boatnet_automations.git` (`automationsDir` on the Pi). On/off and Verbose are plugin-data switches, not YAML keys.
|
|
39
40
|
|
|
40
41
|
```yaml
|
|
41
42
|
helpers:
|
|
@@ -46,18 +47,25 @@ helpers:
|
|
|
46
47
|
latch_on_external_put: electrical.switches.starlink.state
|
|
47
48
|
|
|
48
49
|
automations:
|
|
49
|
-
- id:
|
|
50
|
+
- id: starlink_standby
|
|
50
51
|
trigger:
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
- schedule: "0 4 * * *"
|
|
53
|
+
condition:
|
|
54
|
+
- zone: home_harbour
|
|
55
|
+
- helper: starlink_manual
|
|
56
|
+
is: false
|
|
55
57
|
action:
|
|
56
|
-
- put:
|
|
58
|
+
- put: electrical.switches.starlink.state
|
|
57
59
|
value: true
|
|
60
|
+
- delay: 15m
|
|
61
|
+
- put: electrical.switches.starlink.state
|
|
62
|
+
value: false
|
|
58
63
|
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
```yaml
|
|
66
|
+
- path: electrical.switches.dolphinCharger.voltage
|
|
67
|
+
round: 1 # nearest volt; 230.1 and 230.4 do not both fire
|
|
68
|
+
```
|
|
61
69
|
|
|
62
70
|
```yaml
|
|
63
71
|
- run:
|
|
@@ -5,54 +5,69 @@ helpers:
|
|
|
5
5
|
default: false
|
|
6
6
|
on_start: restore
|
|
7
7
|
latch_on_external_put: electrical.switches.starlink.state
|
|
8
|
-
|
|
8
|
+
depart_prep:
|
|
9
9
|
type: boolean
|
|
10
|
-
name:
|
|
11
|
-
default:
|
|
12
|
-
on_start:
|
|
13
|
-
|
|
14
|
-
zones:
|
|
15
|
-
home_harbour:
|
|
16
|
-
name: Thuishaven
|
|
17
|
-
lat: 52.37
|
|
18
|
-
lon: 5.22
|
|
19
|
-
radius: 150
|
|
10
|
+
name: Maak klaar voor vertrek
|
|
11
|
+
default: false
|
|
12
|
+
on_start: restore
|
|
20
13
|
|
|
21
14
|
automations:
|
|
22
|
-
- id: start_anchorwatch
|
|
23
|
-
alias: Ketting uit → anchorwatch
|
|
24
|
-
enabled: false
|
|
25
|
-
trigger:
|
|
26
|
-
- path: winches.windlass.rode
|
|
27
|
-
above: 2
|
|
28
|
-
unit: m
|
|
29
|
-
for: 10s
|
|
30
|
-
action:
|
|
31
|
-
- put: navigation.anchor.watch
|
|
32
|
-
value: true
|
|
33
|
-
|
|
34
15
|
- id: shore_charge
|
|
35
16
|
alias: Walstroom laadbeleid
|
|
36
|
-
|
|
17
|
+
zones:
|
|
18
|
+
home_harbour:
|
|
19
|
+
name: Thuishaven
|
|
20
|
+
lat: 52.37
|
|
21
|
+
lon: 5.22
|
|
22
|
+
radius: 150
|
|
37
23
|
trigger:
|
|
38
|
-
- path:
|
|
24
|
+
- path: sensors.presence.dolphinshelly
|
|
39
25
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
40
|
-
|
|
26
|
+
round: 0.01
|
|
27
|
+
- helper: depart_prep
|
|
41
28
|
choose:
|
|
42
29
|
- alias: onderweg volladen
|
|
43
30
|
conditions:
|
|
44
|
-
- path:
|
|
31
|
+
- path: sensors.presence.dolphinshelly
|
|
45
32
|
is: true
|
|
46
33
|
- not:
|
|
47
34
|
zone: home_harbour
|
|
48
35
|
action:
|
|
49
36
|
- put: electrical.switches.dolphinCharger.state
|
|
50
37
|
value: 1
|
|
38
|
+
- alias: klaar voor vertrek laden
|
|
39
|
+
conditions:
|
|
40
|
+
- path: sensors.presence.dolphinshelly
|
|
41
|
+
is: true
|
|
42
|
+
- zone: home_harbour
|
|
43
|
+
- helper: depart_prep
|
|
44
|
+
is: true
|
|
45
|
+
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
46
|
+
below: 0.98
|
|
47
|
+
action:
|
|
48
|
+
- put: electrical.switches.dolphinCharger.state
|
|
49
|
+
value: 1
|
|
50
|
+
- alias: klaar voor vertrek vol
|
|
51
|
+
conditions:
|
|
52
|
+
- path: sensors.presence.dolphinshelly
|
|
53
|
+
is: true
|
|
54
|
+
- zone: home_harbour
|
|
55
|
+
- helper: depart_prep
|
|
56
|
+
is: true
|
|
57
|
+
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
58
|
+
above: 0.97
|
|
59
|
+
action:
|
|
60
|
+
- put: electrical.switches.dolphinCharger.state
|
|
61
|
+
value: 0
|
|
62
|
+
- helper: depart_prep
|
|
63
|
+
value: false
|
|
51
64
|
- alias: thuis onder 60%
|
|
52
65
|
conditions:
|
|
53
|
-
- path:
|
|
66
|
+
- path: sensors.presence.dolphinshelly
|
|
54
67
|
is: true
|
|
55
68
|
- zone: home_harbour
|
|
69
|
+
- helper: depart_prep
|
|
70
|
+
is: false
|
|
56
71
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
57
72
|
below: 0.6
|
|
58
73
|
action:
|
|
@@ -60,7 +75,11 @@ automations:
|
|
|
60
75
|
value: 1
|
|
61
76
|
- alias: thuis boven 80%
|
|
62
77
|
conditions:
|
|
78
|
+
- path: sensors.presence.dolphinshelly
|
|
79
|
+
is: true
|
|
63
80
|
- zone: home_harbour
|
|
81
|
+
- helper: depart_prep
|
|
82
|
+
is: false
|
|
64
83
|
- path: electrical.batteries.house.capacity.stateOfCharge
|
|
65
84
|
above: 0.8
|
|
66
85
|
action:
|
|
@@ -69,10 +88,11 @@ automations:
|
|
|
69
88
|
|
|
70
89
|
- id: starlink_standby
|
|
71
90
|
alias: Starlink standby-update
|
|
72
|
-
enabled: false
|
|
73
91
|
trigger:
|
|
74
92
|
- schedule: "0 4 * * *"
|
|
75
93
|
condition:
|
|
94
|
+
# home_harbour is defined on shore_charge (Walstroom laadbeleid).
|
|
95
|
+
- zone: home_harbour
|
|
76
96
|
- helper: starlink_manual
|
|
77
97
|
is: false
|
|
78
98
|
action:
|
|
@@ -81,18 +101,3 @@ automations:
|
|
|
81
101
|
- delay: 15m
|
|
82
102
|
- put: electrical.switches.starlink.state
|
|
83
103
|
value: false
|
|
84
|
-
|
|
85
|
-
- id: harbour_via_script
|
|
86
|
-
alias: Voorbeeld script-conditie
|
|
87
|
-
enabled: false
|
|
88
|
-
trigger:
|
|
89
|
-
- path: electrical.chargers.shore.connected
|
|
90
|
-
is: true
|
|
91
|
-
condition:
|
|
92
|
-
- run: echo.sh
|
|
93
|
-
equals: home
|
|
94
|
-
action:
|
|
95
|
-
- notify:
|
|
96
|
-
path: electrical.chargers.shore
|
|
97
|
-
state: normal
|
|
98
|
-
message: script said home
|
package/lib/engine.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { toSi, parseDuration } = require('./units')
|
|
4
|
-
const { inZone } = require('./geo')
|
|
4
|
+
const { inZone, positionLatLon, zoneDistanceM } = require('./geo')
|
|
5
5
|
const { runScript } = require('./scripts')
|
|
6
6
|
const { helperPath } = require('./helpers')
|
|
7
7
|
|
|
@@ -12,7 +12,9 @@ function collectPaths (doc) {
|
|
|
12
12
|
for (const c of auto.condition || []) addPathsFromClause(paths, c)
|
|
13
13
|
for (const branch of auto.choose || []) {
|
|
14
14
|
for (const c of branch.conditions || []) addPathsFromClause(paths, c)
|
|
15
|
+
for (const action of branch.action || []) addPathsFromAction(paths, action)
|
|
15
16
|
}
|
|
17
|
+
for (const action of auto.action || []) addPathsFromAction(paths, action)
|
|
16
18
|
if (auto.latch_on_external_put) paths.add(auto.latch_on_external_put)
|
|
17
19
|
}
|
|
18
20
|
for (const def of Object.values(doc.helpers || {})) {
|
|
@@ -31,6 +33,12 @@ function addPathsFromClause (paths, clause) {
|
|
|
31
33
|
if (Array.isArray(clause.any)) clause.any.forEach((c) => addPathsFromClause(paths, c))
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
function addPathsFromAction (paths, action) {
|
|
37
|
+
if (!action || typeof action !== 'object') return
|
|
38
|
+
if (action.put) paths.add(action.put)
|
|
39
|
+
if (action.valueFrom) paths.add(action.valueFrom)
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
function getByPath (values, p) {
|
|
35
43
|
if (Object.prototype.hasOwnProperty.call(values, p)) return values[p]
|
|
36
44
|
return undefined
|
|
@@ -81,11 +89,20 @@ function evalClause (clause, ctx) {
|
|
|
81
89
|
if (clause.zone) {
|
|
82
90
|
const pos = getByPath(ctx.values, 'navigation.position')
|
|
83
91
|
const zone = ctx.zones[clause.zone]
|
|
92
|
+
const ll = positionLatLon(pos)
|
|
93
|
+
const distanceM = zone ? zoneDistanceM(pos, zone) : null
|
|
84
94
|
const pass = zone ? inZone(pos, zone) : false
|
|
85
95
|
return {
|
|
86
96
|
pass,
|
|
87
|
-
error: zone ? null : 'Unknown zone: ' + clause.zone,
|
|
88
|
-
actual:
|
|
97
|
+
error: zone ? (ll ? null : 'no GPS position') : 'Unknown zone: ' + clause.zone,
|
|
98
|
+
actual: ll
|
|
99
|
+
? {
|
|
100
|
+
lat: ll.lat,
|
|
101
|
+
lon: ll.lon,
|
|
102
|
+
distance_m: distanceM == null ? null : Math.round(distanceM * 10) / 10,
|
|
103
|
+
radius_m: zone ? Number(zone.radius) : null
|
|
104
|
+
}
|
|
105
|
+
: pos == null ? null : pos,
|
|
89
106
|
path: 'navigation.position',
|
|
90
107
|
expected: clause.zone
|
|
91
108
|
}
|
|
@@ -164,16 +181,128 @@ async function evalClauseAsync (clause, ctx) {
|
|
|
164
181
|
}
|
|
165
182
|
}
|
|
166
183
|
|
|
184
|
+
function fmtVal (value) {
|
|
185
|
+
if (value === undefined) return 'unset'
|
|
186
|
+
try {
|
|
187
|
+
return JSON.stringify(value)
|
|
188
|
+
} catch (_) {
|
|
189
|
+
return String(value)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function clauseLabel (clause, result) {
|
|
194
|
+
if (!clause || typeof clause !== 'object') return ''
|
|
195
|
+
if (clause.not) return 'not ' + clauseLabel(clause.not, result)
|
|
196
|
+
if (Array.isArray(clause.all)) return 'all'
|
|
197
|
+
if (Array.isArray(clause.any)) return 'any'
|
|
198
|
+
if (clause.zone) {
|
|
199
|
+
const dist = result && result.actual && typeof result.actual === 'object'
|
|
200
|
+
? result.actual.distance_m
|
|
201
|
+
: null
|
|
202
|
+
return 'zone ' + clause.zone + (dist != null ? ' (' + dist + ' m)' : '')
|
|
203
|
+
}
|
|
204
|
+
if (clause.helper) {
|
|
205
|
+
let s = 'helper ' + clause.helper
|
|
206
|
+
if (clause.is !== undefined) s += ' is ' + fmtVal(clause.is)
|
|
207
|
+
if (result && result.actual !== undefined) s += ' (' + fmtVal(result.actual) + ')'
|
|
208
|
+
return s
|
|
209
|
+
}
|
|
210
|
+
if (clause.schedule) return 'schedule ' + clause.schedule
|
|
211
|
+
if (clause.run) {
|
|
212
|
+
const file = typeof clause.run === 'string' ? clause.run : clause.run.file
|
|
213
|
+
return 'run ' + (file || '')
|
|
214
|
+
}
|
|
215
|
+
if (clause.path) {
|
|
216
|
+
let s = clause.path
|
|
217
|
+
if (clause.above !== undefined) s += ' > ' + clause.above
|
|
218
|
+
else if (clause.below !== undefined) s += ' < ' + clause.below
|
|
219
|
+
else if (clause.is !== undefined) s += ' is ' + fmtVal(clause.is)
|
|
220
|
+
else if (clause.equals !== undefined) s += ' = ' + fmtVal(clause.equals)
|
|
221
|
+
if (result && result.actual !== undefined && (typeof result.actual !== 'object' || result.actual == null)) {
|
|
222
|
+
s += ' (' + fmtVal(result.actual) + ')'
|
|
223
|
+
}
|
|
224
|
+
return s
|
|
225
|
+
}
|
|
226
|
+
return ''
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function markPass (r) {
|
|
230
|
+
if (!r) return '✗'
|
|
231
|
+
if (r.error) return '✗'
|
|
232
|
+
return r.pass ? '✓' : '✗'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function withLabel (clause, result) {
|
|
236
|
+
const row = result && typeof result === 'object' ? result : { pass: false }
|
|
237
|
+
row.clause = clause
|
|
238
|
+
row.label = clauseLabel(clause, row)
|
|
239
|
+
return row
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function triggerFiredLabel (trigger) {
|
|
243
|
+
if (!trigger) return 'evaluated'
|
|
244
|
+
if (trigger.manual) return 'manual Run'
|
|
245
|
+
if (trigger.start) return 'plugin start'
|
|
246
|
+
if (trigger.schedule) return 'schedule ' + trigger.schedule
|
|
247
|
+
if (trigger.path) return trigger.path + ' = ' + fmtVal(trigger.value)
|
|
248
|
+
return 'evaluated'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isBarePathClause (clause) {
|
|
252
|
+
if (!clause || !clause.path) return false
|
|
253
|
+
return !['is', 'equals', 'above', 'below', 'to'].some((k) =>
|
|
254
|
+
Object.prototype.hasOwnProperty.call(clause, k)
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function triggerListMatched (list, results) {
|
|
259
|
+
if (!(list || []).length) return true
|
|
260
|
+
return (results || []).some((r) => r.pass && !r.error)
|
|
261
|
+
}
|
|
262
|
+
|
|
167
263
|
async function evalList (list, ctx) {
|
|
168
264
|
const results = []
|
|
169
265
|
for (const clause of list || []) {
|
|
170
|
-
const r = await evalClauseAsync(clause, ctx)
|
|
266
|
+
const r = withLabel(clause, await evalClauseAsync(clause, ctx))
|
|
171
267
|
results.push(r)
|
|
172
268
|
if (!r.pass) return { pass: false, results }
|
|
173
269
|
}
|
|
174
270
|
return { pass: true, results }
|
|
175
271
|
}
|
|
176
272
|
|
|
273
|
+
async function evalListAll (list, ctx) {
|
|
274
|
+
const results = []
|
|
275
|
+
for (const clause of list || []) {
|
|
276
|
+
results.push(withLabel(clause, await evalClauseAsync(clause, ctx)))
|
|
277
|
+
}
|
|
278
|
+
const pass = results.every((r) => r.pass && !r.error)
|
|
279
|
+
return { pass, results }
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function formatVerboseLog (auto, record) {
|
|
283
|
+
const name = (auto && (auto.alias || auto.id)) || (record && record.id) || 'automation'
|
|
284
|
+
const result = record.result || 'skipped'
|
|
285
|
+
const head = name + ' → ' + result + (record.reason ? ' (' + record.reason + ')' : '')
|
|
286
|
+
const lines = [head]
|
|
287
|
+
if (record.firedBy) lines.push(' trigger: ' + record.firedBy)
|
|
288
|
+
for (const r of record.triggerResults || []) {
|
|
289
|
+
if (isBarePathClause(r.clause)) continue
|
|
290
|
+
if (!r.label) continue
|
|
291
|
+
lines.push(' ' + markPass(r) + ' ' + r.label)
|
|
292
|
+
}
|
|
293
|
+
for (const r of record.conditions || []) {
|
|
294
|
+
if (!r.label) continue
|
|
295
|
+
lines.push(' ' + markPass(r) + ' ' + r.label)
|
|
296
|
+
}
|
|
297
|
+
for (const b of record.branches || []) {
|
|
298
|
+
lines.push(' ' + (b.picked ? '→ ' : ' ') + b.alias + (b.picked ? '' : ''))
|
|
299
|
+
for (const c of b.conditions || []) {
|
|
300
|
+
lines.push(' ' + markPass(c) + ' ' + (c.label || ''))
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return lines.join('\n')
|
|
304
|
+
}
|
|
305
|
+
|
|
177
306
|
function triggerMatchesPath (auto, changedPath) {
|
|
178
307
|
if (!changedPath) return true
|
|
179
308
|
const paths = new Set()
|
|
@@ -183,6 +312,44 @@ function triggerMatchesPath (auto, changedPath) {
|
|
|
183
312
|
return paths.has(changedPath)
|
|
184
313
|
}
|
|
185
314
|
|
|
315
|
+
function triggerRoundStep (auto, path) {
|
|
316
|
+
if (!path) return null
|
|
317
|
+
for (const t of auto.trigger || []) {
|
|
318
|
+
if (t && t.path === path && t.round != null) return Number(t.round)
|
|
319
|
+
}
|
|
320
|
+
return null
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function quantizeNumber (n, step) {
|
|
324
|
+
const q = Math.round(Number(n) / step) * step
|
|
325
|
+
if (!Number.isFinite(q)) return n
|
|
326
|
+
return Number(q.toPrecision(12))
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function quantizeValue (value, step) {
|
|
330
|
+
const s = Number(step)
|
|
331
|
+
if (!Number.isFinite(s) || s <= 0) return value
|
|
332
|
+
if (typeof value === 'number' && Number.isFinite(value)) return quantizeNumber(value, s)
|
|
333
|
+
const ll = positionLatLon(value)
|
|
334
|
+
if (ll) {
|
|
335
|
+
return {
|
|
336
|
+
latitude: quantizeNumber(ll.lat, s),
|
|
337
|
+
longitude: quantizeNumber(ll.lon, s)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return value
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function quantizedUnchanged (store, auto, path, value) {
|
|
344
|
+
const step = triggerRoundStep(auto, path)
|
|
345
|
+
if (step == null || !Number.isFinite(step) || step <= 0) return false
|
|
346
|
+
const key = auto.id + '\0' + path
|
|
347
|
+
const q = JSON.stringify(quantizeValue(value, step))
|
|
348
|
+
if (store[key] === q) return true
|
|
349
|
+
store[key] = q
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
|
|
186
353
|
function cronMatch (expr, date) {
|
|
187
354
|
if (!expr) return false
|
|
188
355
|
const parts = String(expr).trim().split(/\s+/)
|
|
@@ -216,52 +383,66 @@ function fieldMatch (field, value) {
|
|
|
216
383
|
})
|
|
217
384
|
}
|
|
218
385
|
|
|
386
|
+
function finishRecord (auto, record) {
|
|
387
|
+
record.verboseLog = formatVerboseLog(auto, record)
|
|
388
|
+
return record
|
|
389
|
+
}
|
|
390
|
+
|
|
219
391
|
async function evaluateAutomation (auto, ctx) {
|
|
220
392
|
const record = {
|
|
221
393
|
ts: new Date(ctx.now || Date.now()).toISOString(),
|
|
222
394
|
id: auto.id,
|
|
223
395
|
result: 'skipped',
|
|
224
396
|
trigger: ctx.trigger || null,
|
|
397
|
+
firedBy: triggerFiredLabel(ctx.trigger),
|
|
225
398
|
conditions: [],
|
|
399
|
+
branches: [],
|
|
226
400
|
choose: null,
|
|
227
401
|
actions: []
|
|
228
402
|
}
|
|
229
403
|
if (!ctx.enabled) {
|
|
230
404
|
record.reason = 'disabled'
|
|
231
|
-
return record
|
|
405
|
+
return finishRecord(auto, record)
|
|
232
406
|
}
|
|
233
407
|
|
|
234
|
-
const trig = await
|
|
408
|
+
const trig = await evalListAll(auto.trigger || [], ctx)
|
|
235
409
|
record.triggerResults = trig.results
|
|
236
|
-
if ((auto.trigger
|
|
410
|
+
if (!triggerListMatched(auto.trigger, trig.results)) {
|
|
237
411
|
record.reason = 'trigger not matched'
|
|
238
|
-
return record
|
|
412
|
+
return finishRecord(auto, record)
|
|
239
413
|
}
|
|
240
414
|
|
|
241
|
-
const cond = await
|
|
415
|
+
const cond = await evalListAll(auto.condition || [], ctx)
|
|
242
416
|
record.conditions = cond.results
|
|
243
417
|
if ((auto.condition || []).length && !cond.pass) {
|
|
244
418
|
const failed = cond.results.find((r) => r.error)
|
|
245
419
|
record.result = failed ? 'failure' : 'skipped'
|
|
246
420
|
record.reason = failed ? failed.error : 'condition not met'
|
|
247
|
-
return record
|
|
421
|
+
return finishRecord(auto, record)
|
|
248
422
|
}
|
|
249
423
|
|
|
250
424
|
let actions = auto.action || []
|
|
251
425
|
if (auto.choose && auto.choose.length) {
|
|
252
426
|
let picked = null
|
|
253
427
|
for (const branch of auto.choose) {
|
|
254
|
-
const br = await
|
|
255
|
-
|
|
428
|
+
const br = await evalListAll(branch.conditions || [], ctx)
|
|
429
|
+
const row = {
|
|
430
|
+
alias: branch.alias || branch.id || 'branch',
|
|
431
|
+
pass: br.pass,
|
|
432
|
+
picked: false,
|
|
433
|
+
conditions: br.results
|
|
434
|
+
}
|
|
435
|
+
if (!picked && br.pass) {
|
|
256
436
|
picked = branch
|
|
257
|
-
|
|
437
|
+
row.picked = true
|
|
438
|
+
record.choose = row.alias
|
|
258
439
|
actions = branch.action || []
|
|
259
|
-
break
|
|
260
440
|
}
|
|
441
|
+
record.branches.push(row)
|
|
261
442
|
}
|
|
262
443
|
if (!picked) {
|
|
263
444
|
record.reason = 'no matching choose branch'
|
|
264
|
-
return record
|
|
445
|
+
return finishRecord(auto, record)
|
|
265
446
|
}
|
|
266
447
|
}
|
|
267
448
|
|
|
@@ -271,11 +452,11 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
271
452
|
if (!ran.ok) {
|
|
272
453
|
record.result = 'failure'
|
|
273
454
|
record.reason = ran.error
|
|
274
|
-
return record
|
|
455
|
+
return finishRecord(auto, record)
|
|
275
456
|
}
|
|
276
457
|
}
|
|
277
458
|
record.result = 'ok'
|
|
278
|
-
return record
|
|
459
|
+
return finishRecord(auto, record)
|
|
279
460
|
}
|
|
280
461
|
|
|
281
462
|
async function runAction (action, ctx) {
|
|
@@ -284,15 +465,30 @@ async function runAction (action, ctx) {
|
|
|
284
465
|
}
|
|
285
466
|
if (action.put) {
|
|
286
467
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
468
|
+
const value = Object.prototype.hasOwnProperty.call(action, 'valueFrom')
|
|
469
|
+
? getByPath(ctx.values, action.valueFrom)
|
|
470
|
+
: action.value
|
|
471
|
+
if (Object.prototype.hasOwnProperty.call(action, 'valueFrom') && value === undefined) {
|
|
472
|
+
return { ok: false, type: 'put', path: action.put, error: 'valueFrom missing: ' + action.valueFrom }
|
|
473
|
+
}
|
|
474
|
+
const previous = getByPath(ctx.values, action.put)
|
|
475
|
+
const changed = !sameValue(previous, value)
|
|
476
|
+
if (changed || ctx.skipUnchanged === false) await ctx.put(action.put, value)
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
type: 'put',
|
|
480
|
+
path: action.put,
|
|
481
|
+
value,
|
|
482
|
+
previous,
|
|
483
|
+
changed
|
|
484
|
+
}
|
|
289
485
|
} catch (err) {
|
|
290
486
|
return { ok: false, type: 'put', path: action.put, error: err.message }
|
|
291
487
|
}
|
|
292
488
|
}
|
|
293
489
|
if (action.helper) {
|
|
294
490
|
try {
|
|
295
|
-
ctx.setHelper(action.helper, action.value)
|
|
491
|
+
await ctx.setHelper(action.helper, action.value)
|
|
296
492
|
return { ok: true, type: 'helper', helper: action.helper, value: action.value }
|
|
297
493
|
} catch (err) {
|
|
298
494
|
return { ok: false, type: 'helper', helper: action.helper, error: err.message }
|
|
@@ -309,6 +505,7 @@ async function runAction (action, ctx) {
|
|
|
309
505
|
}
|
|
310
506
|
if (action.delay != null) {
|
|
311
507
|
const ms = parseDuration(action.delay)
|
|
508
|
+
if (ctx.skipDelay) return { ok: true, type: 'delay', ms, skipped: true }
|
|
312
509
|
await ctx.sleep(ms)
|
|
313
510
|
return { ok: true, type: 'delay', ms }
|
|
314
511
|
}
|
|
@@ -326,6 +523,19 @@ async function runAction (action, ctx) {
|
|
|
326
523
|
return { ok: false, error: 'unknown action' }
|
|
327
524
|
}
|
|
328
525
|
|
|
526
|
+
function sameValue (a, b) {
|
|
527
|
+
if (a === b) return true
|
|
528
|
+
if (a == null || b == null) return a === b
|
|
529
|
+
if (typeof a === 'object' || typeof b === 'object') {
|
|
530
|
+
try {
|
|
531
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
532
|
+
} catch (_) {
|
|
533
|
+
return false
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false
|
|
537
|
+
}
|
|
538
|
+
|
|
329
539
|
function makeRunScript (opts) {
|
|
330
540
|
return function (spec) {
|
|
331
541
|
return runScript({
|
|
@@ -343,10 +553,19 @@ module.exports = {
|
|
|
343
553
|
evalClause,
|
|
344
554
|
evalClauseAsync,
|
|
345
555
|
evalList,
|
|
556
|
+
evalListAll,
|
|
346
557
|
evaluateAutomation,
|
|
347
558
|
triggerMatchesPath,
|
|
559
|
+
triggerRoundStep,
|
|
560
|
+
quantizeValue,
|
|
561
|
+
quantizedUnchanged,
|
|
348
562
|
cronMatch,
|
|
349
563
|
runAction,
|
|
350
564
|
makeRunScript,
|
|
351
|
-
parseDuration
|
|
565
|
+
parseDuration,
|
|
566
|
+
sameValue,
|
|
567
|
+
clauseLabel,
|
|
568
|
+
triggerFiredLabel,
|
|
569
|
+
triggerListMatched,
|
|
570
|
+
formatVerboseLog
|
|
352
571
|
}
|
package/lib/geo.js
CHANGED
|
@@ -13,15 +13,23 @@ function haversineM (a, b) {
|
|
|
13
13
|
return 2 * EARTH_M * Math.asin(Math.min(1, Math.sqrt(h)))
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
function positionLatLon (position) {
|
|
17
|
+
if (!position || typeof position !== 'object') return null
|
|
18
|
+
const lat = Number(position.latitude != null ? position.latitude : position.lat)
|
|
19
|
+
const lon = Number(position.longitude != null ? position.longitude : position.lon)
|
|
20
|
+
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null
|
|
21
|
+
return { lat, lon }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function zoneDistanceM (position, zone) {
|
|
25
|
+
const pos = positionLatLon(position)
|
|
26
|
+
if (!pos || !zone) return null
|
|
27
|
+
return haversineM(pos, { lat: Number(zone.lat), lon: Number(zone.lon) })
|
|
28
|
+
}
|
|
29
|
+
|
|
16
30
|
function inZone (position, zone) {
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
const d = haversineM(
|
|
22
|
-
{ lat: position.latitude, lon: position.longitude },
|
|
23
|
-
{ lat: zone.lat, lon: zone.lon }
|
|
24
|
-
)
|
|
31
|
+
const d = zoneDistanceM(position, zone)
|
|
32
|
+
if (d == null || !zone) return false
|
|
25
33
|
return d <= Number(zone.radius)
|
|
26
34
|
}
|
|
27
35
|
|
|
@@ -29,4 +37,4 @@ function toRad (deg) {
|
|
|
29
37
|
return (Number(deg) * Math.PI) / 180
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
module.exports = { haversineM, inZone }
|
|
40
|
+
module.exports = { haversineM, inZone, positionLatLon, zoneDistanceM }
|
package/lib/git.js
CHANGED
|
@@ -46,6 +46,37 @@ function currentSha (dir) {
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
function repoRelPath (repoDir) {
|
|
50
|
+
const root = path.resolve(runGit(['rev-parse', '--show-toplevel'], repoDir))
|
|
51
|
+
const dir = path.resolve(repoDir)
|
|
52
|
+
const rel = path.relative(root, dir).replace(/\\/g, '/')
|
|
53
|
+
if (!rel || rel === '.') return { root, rel: '' }
|
|
54
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
55
|
+
throw new Error('automationsDir is outside the git repository')
|
|
56
|
+
}
|
|
57
|
+
return { root, rel }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function diffRange (repoDir, fromSha, toSha) {
|
|
61
|
+
if (!toSha) throw new Error('Commit sha is required')
|
|
62
|
+
if (!isGitRepo(repoDir)) throw new Error('automationsDir is not a git repository')
|
|
63
|
+
runGit(['cat-file', '-t', toSha], repoDir)
|
|
64
|
+
const from = fromSha || currentSha(repoDir)
|
|
65
|
+
if (!from || from === toSha) {
|
|
66
|
+
return { from: from || null, to: toSha, diff: '' }
|
|
67
|
+
}
|
|
68
|
+
runGit(['cat-file', '-t', from], repoDir)
|
|
69
|
+
const { root, rel } = repoRelPath(repoDir)
|
|
70
|
+
const args = ['diff', '--find-renames', from, toSha]
|
|
71
|
+
if (rel) args.push('--', rel)
|
|
72
|
+
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 2 * 1024 * 1024 })
|
|
73
|
+
if (r.status !== 0 && r.status !== 1) {
|
|
74
|
+
const err = (r.stderr || r.stdout || '').trim()
|
|
75
|
+
throw new Error('git diff failed: ' + err)
|
|
76
|
+
}
|
|
77
|
+
return { from, to: toSha, diff: (r.stdout || '').replace(/\s+$/, '') }
|
|
78
|
+
}
|
|
79
|
+
|
|
49
80
|
function archiveTreeish (repoDir, sha) {
|
|
50
81
|
const root = path.resolve(runGit(['rev-parse', '--show-toplevel'], repoDir))
|
|
51
82
|
const dir = path.resolve(repoDir)
|
|
@@ -108,5 +139,6 @@ module.exports = {
|
|
|
108
139
|
listCommits,
|
|
109
140
|
currentSha,
|
|
110
141
|
archiveTreeish,
|
|
111
|
-
materializeCommit
|
|
142
|
+
materializeCommit,
|
|
143
|
+
diffRange
|
|
112
144
|
}
|