signalk-automation-plugin 0.0.1 → 0.0.2
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 +16 -8
- package/examples/automations.yaml +49 -44
- package/lib/engine.js +233 -20
- package/lib/geo.js +17 -9
- package/lib/git.js +33 -1
- package/lib/runtime.js +215 -21
- 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 +493 -72
- package/public/icon.png +0 -0
- package/public/index.html +48 -21
- package/public/style.css +195 -20
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ Not Node-RED. Not a Home Assistant YAML clone. Actions are Signal K PUTs, helper
|
|
|
12
12
|
- `default` — set `default` on every start
|
|
13
13
|
- `none` — do nothing on start
|
|
14
14
|
- Path triggers, `choose`, zones, cron `schedule`
|
|
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,123 @@ 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
|
+
|
|
167
258
|
async function evalList (list, ctx) {
|
|
168
259
|
const results = []
|
|
169
260
|
for (const clause of list || []) {
|
|
170
|
-
const r = await evalClauseAsync(clause, ctx)
|
|
261
|
+
const r = withLabel(clause, await evalClauseAsync(clause, ctx))
|
|
171
262
|
results.push(r)
|
|
172
263
|
if (!r.pass) return { pass: false, results }
|
|
173
264
|
}
|
|
174
265
|
return { pass: true, results }
|
|
175
266
|
}
|
|
176
267
|
|
|
268
|
+
async function evalListAll (list, ctx) {
|
|
269
|
+
const results = []
|
|
270
|
+
for (const clause of list || []) {
|
|
271
|
+
results.push(withLabel(clause, await evalClauseAsync(clause, ctx)))
|
|
272
|
+
}
|
|
273
|
+
const pass = results.every((r) => r.pass && !r.error)
|
|
274
|
+
return { pass, results }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function formatVerboseLog (auto, record) {
|
|
278
|
+
const name = (auto && (auto.alias || auto.id)) || (record && record.id) || 'automation'
|
|
279
|
+
const result = record.result || 'skipped'
|
|
280
|
+
const head = name + ' → ' + result + (record.reason ? ' (' + record.reason + ')' : '')
|
|
281
|
+
const lines = [head]
|
|
282
|
+
if (record.firedBy) lines.push(' trigger: ' + record.firedBy)
|
|
283
|
+
for (const r of record.triggerResults || []) {
|
|
284
|
+
if (isBarePathClause(r.clause)) continue
|
|
285
|
+
if (!r.label) continue
|
|
286
|
+
lines.push(' ' + markPass(r) + ' ' + r.label)
|
|
287
|
+
}
|
|
288
|
+
for (const r of record.conditions || []) {
|
|
289
|
+
if (!r.label) continue
|
|
290
|
+
lines.push(' ' + markPass(r) + ' ' + r.label)
|
|
291
|
+
}
|
|
292
|
+
for (const b of record.branches || []) {
|
|
293
|
+
lines.push(' ' + (b.picked ? '→ ' : ' ') + b.alias + (b.picked ? '' : ''))
|
|
294
|
+
for (const c of b.conditions || []) {
|
|
295
|
+
lines.push(' ' + markPass(c) + ' ' + (c.label || ''))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return lines.join('\n')
|
|
299
|
+
}
|
|
300
|
+
|
|
177
301
|
function triggerMatchesPath (auto, changedPath) {
|
|
178
302
|
if (!changedPath) return true
|
|
179
303
|
const paths = new Set()
|
|
@@ -183,6 +307,44 @@ function triggerMatchesPath (auto, changedPath) {
|
|
|
183
307
|
return paths.has(changedPath)
|
|
184
308
|
}
|
|
185
309
|
|
|
310
|
+
function triggerRoundStep (auto, path) {
|
|
311
|
+
if (!path) return null
|
|
312
|
+
for (const t of auto.trigger || []) {
|
|
313
|
+
if (t && t.path === path && t.round != null) return Number(t.round)
|
|
314
|
+
}
|
|
315
|
+
return null
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function quantizeNumber (n, step) {
|
|
319
|
+
const q = Math.round(Number(n) / step) * step
|
|
320
|
+
if (!Number.isFinite(q)) return n
|
|
321
|
+
return Number(q.toPrecision(12))
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function quantizeValue (value, step) {
|
|
325
|
+
const s = Number(step)
|
|
326
|
+
if (!Number.isFinite(s) || s <= 0) return value
|
|
327
|
+
if (typeof value === 'number' && Number.isFinite(value)) return quantizeNumber(value, s)
|
|
328
|
+
const ll = positionLatLon(value)
|
|
329
|
+
if (ll) {
|
|
330
|
+
return {
|
|
331
|
+
latitude: quantizeNumber(ll.lat, s),
|
|
332
|
+
longitude: quantizeNumber(ll.lon, s)
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return value
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function quantizedUnchanged (store, auto, path, value) {
|
|
339
|
+
const step = triggerRoundStep(auto, path)
|
|
340
|
+
if (step == null || !Number.isFinite(step) || step <= 0) return false
|
|
341
|
+
const key = auto.id + '\0' + path
|
|
342
|
+
const q = JSON.stringify(quantizeValue(value, step))
|
|
343
|
+
if (store[key] === q) return true
|
|
344
|
+
store[key] = q
|
|
345
|
+
return false
|
|
346
|
+
}
|
|
347
|
+
|
|
186
348
|
function cronMatch (expr, date) {
|
|
187
349
|
if (!expr) return false
|
|
188
350
|
const parts = String(expr).trim().split(/\s+/)
|
|
@@ -216,52 +378,66 @@ function fieldMatch (field, value) {
|
|
|
216
378
|
})
|
|
217
379
|
}
|
|
218
380
|
|
|
381
|
+
function finishRecord (auto, record) {
|
|
382
|
+
record.verboseLog = formatVerboseLog(auto, record)
|
|
383
|
+
return record
|
|
384
|
+
}
|
|
385
|
+
|
|
219
386
|
async function evaluateAutomation (auto, ctx) {
|
|
220
387
|
const record = {
|
|
221
388
|
ts: new Date(ctx.now || Date.now()).toISOString(),
|
|
222
389
|
id: auto.id,
|
|
223
390
|
result: 'skipped',
|
|
224
391
|
trigger: ctx.trigger || null,
|
|
392
|
+
firedBy: triggerFiredLabel(ctx.trigger),
|
|
225
393
|
conditions: [],
|
|
394
|
+
branches: [],
|
|
226
395
|
choose: null,
|
|
227
396
|
actions: []
|
|
228
397
|
}
|
|
229
398
|
if (!ctx.enabled) {
|
|
230
399
|
record.reason = 'disabled'
|
|
231
|
-
return record
|
|
400
|
+
return finishRecord(auto, record)
|
|
232
401
|
}
|
|
233
402
|
|
|
234
|
-
const trig = await
|
|
403
|
+
const trig = await evalListAll(auto.trigger || [], ctx)
|
|
235
404
|
record.triggerResults = trig.results
|
|
236
405
|
if ((auto.trigger || []).length && !trig.pass) {
|
|
237
406
|
record.reason = 'trigger not matched'
|
|
238
|
-
return record
|
|
407
|
+
return finishRecord(auto, record)
|
|
239
408
|
}
|
|
240
409
|
|
|
241
|
-
const cond = await
|
|
410
|
+
const cond = await evalListAll(auto.condition || [], ctx)
|
|
242
411
|
record.conditions = cond.results
|
|
243
412
|
if ((auto.condition || []).length && !cond.pass) {
|
|
244
413
|
const failed = cond.results.find((r) => r.error)
|
|
245
414
|
record.result = failed ? 'failure' : 'skipped'
|
|
246
415
|
record.reason = failed ? failed.error : 'condition not met'
|
|
247
|
-
return record
|
|
416
|
+
return finishRecord(auto, record)
|
|
248
417
|
}
|
|
249
418
|
|
|
250
419
|
let actions = auto.action || []
|
|
251
420
|
if (auto.choose && auto.choose.length) {
|
|
252
421
|
let picked = null
|
|
253
422
|
for (const branch of auto.choose) {
|
|
254
|
-
const br = await
|
|
255
|
-
|
|
423
|
+
const br = await evalListAll(branch.conditions || [], ctx)
|
|
424
|
+
const row = {
|
|
425
|
+
alias: branch.alias || branch.id || 'branch',
|
|
426
|
+
pass: br.pass,
|
|
427
|
+
picked: false,
|
|
428
|
+
conditions: br.results
|
|
429
|
+
}
|
|
430
|
+
if (!picked && br.pass) {
|
|
256
431
|
picked = branch
|
|
257
|
-
|
|
432
|
+
row.picked = true
|
|
433
|
+
record.choose = row.alias
|
|
258
434
|
actions = branch.action || []
|
|
259
|
-
break
|
|
260
435
|
}
|
|
436
|
+
record.branches.push(row)
|
|
261
437
|
}
|
|
262
438
|
if (!picked) {
|
|
263
439
|
record.reason = 'no matching choose branch'
|
|
264
|
-
return record
|
|
440
|
+
return finishRecord(auto, record)
|
|
265
441
|
}
|
|
266
442
|
}
|
|
267
443
|
|
|
@@ -271,11 +447,11 @@ async function evaluateAutomation (auto, ctx) {
|
|
|
271
447
|
if (!ran.ok) {
|
|
272
448
|
record.result = 'failure'
|
|
273
449
|
record.reason = ran.error
|
|
274
|
-
return record
|
|
450
|
+
return finishRecord(auto, record)
|
|
275
451
|
}
|
|
276
452
|
}
|
|
277
453
|
record.result = 'ok'
|
|
278
|
-
return record
|
|
454
|
+
return finishRecord(auto, record)
|
|
279
455
|
}
|
|
280
456
|
|
|
281
457
|
async function runAction (action, ctx) {
|
|
@@ -284,15 +460,30 @@ async function runAction (action, ctx) {
|
|
|
284
460
|
}
|
|
285
461
|
if (action.put) {
|
|
286
462
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
463
|
+
const value = Object.prototype.hasOwnProperty.call(action, 'valueFrom')
|
|
464
|
+
? getByPath(ctx.values, action.valueFrom)
|
|
465
|
+
: action.value
|
|
466
|
+
if (Object.prototype.hasOwnProperty.call(action, 'valueFrom') && value === undefined) {
|
|
467
|
+
return { ok: false, type: 'put', path: action.put, error: 'valueFrom missing: ' + action.valueFrom }
|
|
468
|
+
}
|
|
469
|
+
const previous = getByPath(ctx.values, action.put)
|
|
470
|
+
const changed = !sameValue(previous, value)
|
|
471
|
+
if (changed || ctx.skipUnchanged === false) await ctx.put(action.put, value)
|
|
472
|
+
return {
|
|
473
|
+
ok: true,
|
|
474
|
+
type: 'put',
|
|
475
|
+
path: action.put,
|
|
476
|
+
value,
|
|
477
|
+
previous,
|
|
478
|
+
changed
|
|
479
|
+
}
|
|
289
480
|
} catch (err) {
|
|
290
481
|
return { ok: false, type: 'put', path: action.put, error: err.message }
|
|
291
482
|
}
|
|
292
483
|
}
|
|
293
484
|
if (action.helper) {
|
|
294
485
|
try {
|
|
295
|
-
ctx.setHelper(action.helper, action.value)
|
|
486
|
+
await ctx.setHelper(action.helper, action.value)
|
|
296
487
|
return { ok: true, type: 'helper', helper: action.helper, value: action.value }
|
|
297
488
|
} catch (err) {
|
|
298
489
|
return { ok: false, type: 'helper', helper: action.helper, error: err.message }
|
|
@@ -309,6 +500,7 @@ async function runAction (action, ctx) {
|
|
|
309
500
|
}
|
|
310
501
|
if (action.delay != null) {
|
|
311
502
|
const ms = parseDuration(action.delay)
|
|
503
|
+
if (ctx.skipDelay) return { ok: true, type: 'delay', ms, skipped: true }
|
|
312
504
|
await ctx.sleep(ms)
|
|
313
505
|
return { ok: true, type: 'delay', ms }
|
|
314
506
|
}
|
|
@@ -326,6 +518,19 @@ async function runAction (action, ctx) {
|
|
|
326
518
|
return { ok: false, error: 'unknown action' }
|
|
327
519
|
}
|
|
328
520
|
|
|
521
|
+
function sameValue (a, b) {
|
|
522
|
+
if (a === b) return true
|
|
523
|
+
if (a == null || b == null) return a === b
|
|
524
|
+
if (typeof a === 'object' || typeof b === 'object') {
|
|
525
|
+
try {
|
|
526
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
527
|
+
} catch (_) {
|
|
528
|
+
return false
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return false
|
|
532
|
+
}
|
|
533
|
+
|
|
329
534
|
function makeRunScript (opts) {
|
|
330
535
|
return function (spec) {
|
|
331
536
|
return runScript({
|
|
@@ -343,10 +548,18 @@ module.exports = {
|
|
|
343
548
|
evalClause,
|
|
344
549
|
evalClauseAsync,
|
|
345
550
|
evalList,
|
|
551
|
+
evalListAll,
|
|
346
552
|
evaluateAutomation,
|
|
347
553
|
triggerMatchesPath,
|
|
554
|
+
triggerRoundStep,
|
|
555
|
+
quantizeValue,
|
|
556
|
+
quantizedUnchanged,
|
|
348
557
|
cronMatch,
|
|
349
558
|
runAction,
|
|
350
559
|
makeRunScript,
|
|
351
|
-
parseDuration
|
|
560
|
+
parseDuration,
|
|
561
|
+
sameValue,
|
|
562
|
+
clauseLabel,
|
|
563
|
+
triggerFiredLabel,
|
|
564
|
+
formatVerboseLog
|
|
352
565
|
}
|
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
|
}
|