signalk-automation-plugin 0.0.1
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/LICENSE +15 -0
- package/README.md +76 -0
- package/examples/automations.yaml +98 -0
- package/examples/scripts/echo.sh +2 -0
- package/examples/scripts/fail.sh +3 -0
- package/lib/engine.js +352 -0
- package/lib/geo.js +32 -0
- package/lib/git.js +112 -0
- package/lib/helpers.js +60 -0
- package/lib/runtime.js +270 -0
- package/lib/scripts.js +120 -0
- package/lib/store.js +75 -0
- package/lib/units.js +40 -0
- package/lib/yaml.js +204 -0
- package/package.json +40 -0
- package/plugin/index.js +340 -0
- package/public/app.js +155 -0
- package/public/index.html +39 -0
- package/public/style.css +70 -0
package/lib/yaml.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const yaml = require('js-yaml')
|
|
6
|
+
|
|
7
|
+
const ON_START = ['restore', 'default', 'none']
|
|
8
|
+
const HELPER_TYPES = ['boolean', 'number', 'string', 'select']
|
|
9
|
+
|
|
10
|
+
function loadYamlDir (dir) {
|
|
11
|
+
const result = emptyDoc()
|
|
12
|
+
if (!dir || !fs.existsSync(dir)) {
|
|
13
|
+
return result
|
|
14
|
+
}
|
|
15
|
+
const files = fs
|
|
16
|
+
.readdirSync(dir)
|
|
17
|
+
.filter((f) => /\.ya?ml$/i.test(f))
|
|
18
|
+
.sort()
|
|
19
|
+
for (const file of files) {
|
|
20
|
+
const full = path.join(dir, file)
|
|
21
|
+
let raw
|
|
22
|
+
try {
|
|
23
|
+
raw = fs.readFileSync(full, 'utf8')
|
|
24
|
+
} catch (err) {
|
|
25
|
+
result.errors.push({ file, message: err.message })
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
const loaded = loadYamlText(raw, file)
|
|
29
|
+
mergeDoc(result, loaded)
|
|
30
|
+
}
|
|
31
|
+
return result
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadYamlText (text, file) {
|
|
35
|
+
const result = emptyDoc()
|
|
36
|
+
result.files.push(file)
|
|
37
|
+
let parsed
|
|
38
|
+
try {
|
|
39
|
+
parsed = yaml.load(text) || {}
|
|
40
|
+
} catch (err) {
|
|
41
|
+
result.errors.push({ file, message: err.message })
|
|
42
|
+
return result
|
|
43
|
+
}
|
|
44
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
45
|
+
result.errors.push({ file, message: 'YAML root must be a mapping' })
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
result.rawText[file] = text
|
|
49
|
+
mergeHelpers(result, parsed.helpers, file)
|
|
50
|
+
mergeZones(result, parsed.zones, file)
|
|
51
|
+
mergeAutomations(result, parsed.automations, file)
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function emptyDoc () {
|
|
56
|
+
return {
|
|
57
|
+
helpers: {},
|
|
58
|
+
zones: {},
|
|
59
|
+
automations: [],
|
|
60
|
+
errors: [],
|
|
61
|
+
files: [],
|
|
62
|
+
rawText: {}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function mergeDoc (into, other) {
|
|
67
|
+
into.files.push.apply(into.files, other.files)
|
|
68
|
+
into.errors.push.apply(into.errors, other.errors)
|
|
69
|
+
Object.assign(into.rawText, other.rawText)
|
|
70
|
+
for (const [id, def] of Object.entries(other.helpers)) {
|
|
71
|
+
if (into.helpers[id]) {
|
|
72
|
+
into.errors.push({ file: def.file, message: 'Duplicate helper id: ' + id })
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
into.helpers[id] = def
|
|
76
|
+
}
|
|
77
|
+
for (const [id, def] of Object.entries(other.zones)) {
|
|
78
|
+
if (into.zones[id]) {
|
|
79
|
+
into.errors.push({ file: def.file, message: 'Duplicate zone id: ' + id })
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
into.zones[id] = def
|
|
83
|
+
}
|
|
84
|
+
const seen = new Set(into.automations.map((a) => a.id))
|
|
85
|
+
for (const auto of other.automations) {
|
|
86
|
+
if (seen.has(auto.id)) {
|
|
87
|
+
into.errors.push({ file: auto.file, message: 'Duplicate automation id: ' + auto.id })
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
seen.add(auto.id)
|
|
91
|
+
into.automations.push(auto)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function mergeHelpers (result, helpers, file) {
|
|
96
|
+
if (helpers == null) return
|
|
97
|
+
if (typeof helpers !== 'object' || Array.isArray(helpers)) {
|
|
98
|
+
result.errors.push({ file, message: 'helpers must be a mapping' })
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
for (const [id, raw] of Object.entries(helpers)) {
|
|
102
|
+
const def = normalizeHelper(id, raw, file)
|
|
103
|
+
if (def.error) {
|
|
104
|
+
result.errors.push({ file, message: def.error })
|
|
105
|
+
continue
|
|
106
|
+
}
|
|
107
|
+
result.helpers[id] = def
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function normalizeHelper (id, raw, file) {
|
|
112
|
+
const spec = raw && typeof raw === 'object' ? raw : {}
|
|
113
|
+
const type = spec.type || 'boolean'
|
|
114
|
+
if (!HELPER_TYPES.includes(type)) {
|
|
115
|
+
return { error: 'Helper ' + id + ': unknown type ' + type }
|
|
116
|
+
}
|
|
117
|
+
const onStart = spec.on_start || spec.onStart || 'none'
|
|
118
|
+
if (!ON_START.includes(onStart)) {
|
|
119
|
+
return { error: 'Helper ' + id + ': on_start must be restore|default|none' }
|
|
120
|
+
}
|
|
121
|
+
if (type === 'select' && !Array.isArray(spec.options)) {
|
|
122
|
+
return { error: 'Helper ' + id + ': select requires options' }
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
id,
|
|
126
|
+
file,
|
|
127
|
+
type,
|
|
128
|
+
name: spec.name || id,
|
|
129
|
+
default: spec.default,
|
|
130
|
+
on_start: onStart,
|
|
131
|
+
options: spec.options || [],
|
|
132
|
+
latch_on_external_put: spec.latch_on_external_put || spec.latchOnExternalPut || null
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function mergeZones (result, zones, file) {
|
|
137
|
+
if (zones == null) return
|
|
138
|
+
if (typeof zones !== 'object' || Array.isArray(zones)) {
|
|
139
|
+
result.errors.push({ file, message: 'zones must be a mapping' })
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
for (const [id, raw] of Object.entries(zones)) {
|
|
143
|
+
const spec = raw || {}
|
|
144
|
+
const lat = Number(spec.lat)
|
|
145
|
+
const lon = Number(spec.lon)
|
|
146
|
+
const radius = Number(spec.radius)
|
|
147
|
+
if (![lat, lon, radius].every(Number.isFinite)) {
|
|
148
|
+
result.errors.push({ file, message: 'Zone ' + id + ' needs lat, lon, radius' })
|
|
149
|
+
continue
|
|
150
|
+
}
|
|
151
|
+
result.zones[id] = { id, file, lat, lon, radius, name: spec.name || id }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function mergeAutomations (result, list, file) {
|
|
156
|
+
if (list == null) return
|
|
157
|
+
if (!Array.isArray(list)) {
|
|
158
|
+
result.errors.push({ file, message: 'automations must be a list' })
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
for (const raw of list) {
|
|
162
|
+
if (!raw || typeof raw !== 'object' || !raw.id) {
|
|
163
|
+
result.errors.push({ file, message: 'Automation needs an id' })
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
result.automations.push({
|
|
167
|
+
id: String(raw.id),
|
|
168
|
+
file,
|
|
169
|
+
alias: raw.alias || raw.id,
|
|
170
|
+
enabled: raw.enabled !== false,
|
|
171
|
+
trigger: asArray(raw.trigger),
|
|
172
|
+
condition: asArray(raw.condition),
|
|
173
|
+
action: asArray(raw.action),
|
|
174
|
+
choose: Array.isArray(raw.choose) ? raw.choose : [],
|
|
175
|
+
debounceMs: raw.debounceMs || 0,
|
|
176
|
+
edge: raw.edge || 'rising'
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function asArray (value) {
|
|
182
|
+
if (value == null) return []
|
|
183
|
+
return Array.isArray(value) ? value : [value]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function combinedYaml (doc) {
|
|
187
|
+
const parts = []
|
|
188
|
+
for (const file of doc.files) {
|
|
189
|
+
const text = doc.rawText[file]
|
|
190
|
+
if (text) {
|
|
191
|
+
parts.push('# file: ' + file + '\n' + text.trim())
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return parts.join('\n\n---\n\n')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = {
|
|
198
|
+
ON_START,
|
|
199
|
+
HELPER_TYPES,
|
|
200
|
+
loadYamlDir,
|
|
201
|
+
loadYamlText,
|
|
202
|
+
emptyDoc,
|
|
203
|
+
combinedYaml
|
|
204
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "signalk-automation-plugin",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Home Assistant-like YAML automations for Signal K",
|
|
5
|
+
"main": "plugin/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"signalk-node-server-plugin",
|
|
8
|
+
"signalk-webapp",
|
|
9
|
+
"signalk-category-digital-switching",
|
|
10
|
+
"automation"
|
|
11
|
+
],
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"author": "hans@defect.org",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/htool/signalk-automation-plugin.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/htool/signalk-automation-plugin/issues"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/htool/signalk-automation-plugin#readme",
|
|
22
|
+
"signalk": {
|
|
23
|
+
"displayName": "Automations"
|
|
24
|
+
},
|
|
25
|
+
"signalk-plugin-enabled-by-default": false,
|
|
26
|
+
"files": [
|
|
27
|
+
"plugin",
|
|
28
|
+
"lib",
|
|
29
|
+
"public",
|
|
30
|
+
"examples",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test test/*.test.js && node scripts/test-npm-publish-check.js"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"js-yaml": "^4.1.0"
|
|
39
|
+
}
|
|
40
|
+
}
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const { Runtime } = require('../lib/runtime')
|
|
5
|
+
const helpers = require('../lib/helpers')
|
|
6
|
+
|
|
7
|
+
const PLUGIN_ID = 'signalk-automation-plugin'
|
|
8
|
+
|
|
9
|
+
function sendJson (res, body, status) {
|
|
10
|
+
res.statusCode = status || 200
|
|
11
|
+
res.setHeader('Content-Type', 'application/json')
|
|
12
|
+
res.end(JSON.stringify(body))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readJson (req) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
if (req.body && typeof req.body === 'object' && !Buffer.isBuffer(req.body)) {
|
|
18
|
+
resolve(req.body)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
let raw = ''
|
|
22
|
+
req.on('data', (c) => {
|
|
23
|
+
raw += c
|
|
24
|
+
if (raw.length > 1e6) {
|
|
25
|
+
reject(new Error('body too large'))
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
req.on('end', () => {
|
|
29
|
+
if (!raw) {
|
|
30
|
+
resolve({})
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
resolve(JSON.parse(raw))
|
|
35
|
+
} catch (err) {
|
|
36
|
+
reject(err)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
req.on('error', reject)
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = function (app) {
|
|
44
|
+
const plugin = {}
|
|
45
|
+
plugin.id = PLUGIN_ID
|
|
46
|
+
plugin.name = 'Automations'
|
|
47
|
+
plugin.description =
|
|
48
|
+
'YAML automations for Signal K: helpers, path triggers, PUT/notify/script actions, git-live YAML.'
|
|
49
|
+
|
|
50
|
+
let runtime = null
|
|
51
|
+
let options = {}
|
|
52
|
+
const timers = []
|
|
53
|
+
const unsubscribes = []
|
|
54
|
+
|
|
55
|
+
plugin.schema = {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
automationsDir: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
title: 'Automations git / YAML directory',
|
|
61
|
+
description: 'Folder with *.yaml (preferably a git repo). Live snapshot is stored in plugin data.',
|
|
62
|
+
default: ''
|
|
63
|
+
},
|
|
64
|
+
scriptsDir: {
|
|
65
|
+
type: 'string',
|
|
66
|
+
title: 'Allowed scripts directory',
|
|
67
|
+
description: 'run: actions may only execute files under this path (relative paths in YAML).',
|
|
68
|
+
default: ''
|
|
69
|
+
},
|
|
70
|
+
traceCount: {
|
|
71
|
+
type: 'number',
|
|
72
|
+
title: 'Decision records to keep per automation',
|
|
73
|
+
default: 20,
|
|
74
|
+
minimum: 1
|
|
75
|
+
},
|
|
76
|
+
scriptTimeoutSeconds: {
|
|
77
|
+
type: 'number',
|
|
78
|
+
title: 'Script timeout (seconds)',
|
|
79
|
+
default: 30,
|
|
80
|
+
minimum: 1
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function snapshot () {
|
|
86
|
+
if (!runtime) {
|
|
87
|
+
return {
|
|
88
|
+
started: false,
|
|
89
|
+
automations: [],
|
|
90
|
+
helpers: [],
|
|
91
|
+
errors: [],
|
|
92
|
+
yaml: '',
|
|
93
|
+
commits: []
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return runtime.snapshot()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function requireRuntime (res) {
|
|
100
|
+
if (!runtime) {
|
|
101
|
+
sendJson(res, { error: 'plugin not started' }, 409)
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
return runtime
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleLive (req, res) {
|
|
108
|
+
const rt = requireRuntime(res)
|
|
109
|
+
if (!rt) return
|
|
110
|
+
try {
|
|
111
|
+
const body = await readJson(req)
|
|
112
|
+
const snap = rt.activateCommit(body.sha)
|
|
113
|
+
sendJson(res, snap)
|
|
114
|
+
} catch (err) {
|
|
115
|
+
sendJson(res, { error: err.message }, 400)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleEnabled (req, res, id) {
|
|
120
|
+
const rt = requireRuntime(res)
|
|
121
|
+
if (!rt) return
|
|
122
|
+
try {
|
|
123
|
+
const body = await readJson(req)
|
|
124
|
+
rt.setEnabled(id, body.enabled)
|
|
125
|
+
sendJson(res, { id, enabled: rt.isEnabled({ id, enabled: true }) && body.enabled })
|
|
126
|
+
} catch (err) {
|
|
127
|
+
sendJson(res, { error: err.message }, 400)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function handleHelperPut (req, res, id) {
|
|
132
|
+
const rt = requireRuntime(res)
|
|
133
|
+
if (!rt) return
|
|
134
|
+
try {
|
|
135
|
+
const body = await readJson(req)
|
|
136
|
+
const value = rt.setHelper(id, body.value)
|
|
137
|
+
emitHelper(id, value)
|
|
138
|
+
sendJson(res, { id, value })
|
|
139
|
+
} catch (err) {
|
|
140
|
+
sendJson(res, { error: err.message }, 400)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleHelperReset (req, res, id) {
|
|
145
|
+
const rt = requireRuntime(res)
|
|
146
|
+
if (!rt) return
|
|
147
|
+
try {
|
|
148
|
+
const value = rt.resetHelper(id)
|
|
149
|
+
emitHelper(id, value)
|
|
150
|
+
sendJson(res, { id, value: value == null ? null : value })
|
|
151
|
+
} catch (err) {
|
|
152
|
+
sendJson(res, { error: err.message }, 400)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
plugin.registerWithRouter = function (router) {
|
|
157
|
+
router.get('/status', (req, res) => sendJson(res, snapshot()))
|
|
158
|
+
router.get('/yaml', (req, res) => sendJson(res, { yaml: snapshot().yaml, sha: snapshot().activeSha }))
|
|
159
|
+
router.get('/commits', (req, res) => sendJson(res, { commits: snapshot().commits, activeSha: snapshot().activeSha }))
|
|
160
|
+
router.get('/automations/:id/traces', (req, res) => {
|
|
161
|
+
const rt = requireRuntime(res)
|
|
162
|
+
if (!rt) return
|
|
163
|
+
sendJson(res, { id: req.params.id, traces: rt.traces(req.params.id) })
|
|
164
|
+
})
|
|
165
|
+
router.post('/live', (req, res) => {
|
|
166
|
+
handleLive(req, res)
|
|
167
|
+
})
|
|
168
|
+
router.post('/automations/:id/enabled', (req, res) => {
|
|
169
|
+
handleEnabled(req, res, req.params.id)
|
|
170
|
+
})
|
|
171
|
+
router.put('/helpers/:id', (req, res) => {
|
|
172
|
+
handleHelperPut(req, res, req.params.id)
|
|
173
|
+
})
|
|
174
|
+
router.post('/helpers/:id/reset', (req, res) => {
|
|
175
|
+
handleHelperReset(req, res, req.params.id)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
plugin.signalKApiRoutes = function (router) {
|
|
180
|
+
const prefix = '/' + PLUGIN_ID
|
|
181
|
+
router.get(prefix + '/status', (req, res) => sendJson(res, snapshot()))
|
|
182
|
+
router.get(prefix + '/yaml', (req, res) =>
|
|
183
|
+
sendJson(res, { yaml: snapshot().yaml, sha: snapshot().activeSha })
|
|
184
|
+
)
|
|
185
|
+
router.get(prefix + '/commits', (req, res) =>
|
|
186
|
+
sendJson(res, { commits: snapshot().commits, activeSha: snapshot().activeSha })
|
|
187
|
+
)
|
|
188
|
+
router.get(prefix + '/automations/:id/traces', (req, res) => {
|
|
189
|
+
if (!runtime) {
|
|
190
|
+
sendJson(res, { error: 'plugin not started' }, 409)
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
sendJson(res, { id: req.params.id, traces: runtime.traces(req.params.id) })
|
|
194
|
+
})
|
|
195
|
+
return router
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function emitHelper (id, value) {
|
|
199
|
+
const def = runtime && runtime.doc.helpers[id]
|
|
200
|
+
app.handleMessage(PLUGIN_ID, {
|
|
201
|
+
updates: [
|
|
202
|
+
{
|
|
203
|
+
values: [{ path: helpers.helperPath(id), value: value == null ? null : value }],
|
|
204
|
+
meta: [
|
|
205
|
+
{
|
|
206
|
+
path: helpers.helperPath(id),
|
|
207
|
+
value: {
|
|
208
|
+
displayName: def ? def.name : id,
|
|
209
|
+
supportsPut: true
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function putPath (p, value) {
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
if (typeof app.putSelfPath === 'function') {
|
|
221
|
+
app.putSelfPath(p, value, (err) => (err ? reject(err) : resolve()))
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
app.handleMessage(PLUGIN_ID, {
|
|
225
|
+
updates: [{ values: [{ path: p, value }] }]
|
|
226
|
+
})
|
|
227
|
+
resolve()
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function notify (p, state, message) {
|
|
232
|
+
const npath = String(p).startsWith('notifications.') ? p : 'notifications.' + p
|
|
233
|
+
app.handleMessage(PLUGIN_ID, {
|
|
234
|
+
updates: [
|
|
235
|
+
{
|
|
236
|
+
values: [
|
|
237
|
+
{
|
|
238
|
+
path: npath,
|
|
239
|
+
value: {
|
|
240
|
+
state: state || 'alert',
|
|
241
|
+
message: message || '',
|
|
242
|
+
method: ['visual', 'sound']
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
})
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
plugin.start = function (opts) {
|
|
252
|
+
options = opts || {}
|
|
253
|
+
const dataDir =
|
|
254
|
+
(app.getDataDirPath && app.getDataDirPath()) ||
|
|
255
|
+
path.join(process.cwd(), 'plugin-data', PLUGIN_ID)
|
|
256
|
+
fsMkdir(dataDir)
|
|
257
|
+
|
|
258
|
+
runtime = new Runtime({
|
|
259
|
+
app,
|
|
260
|
+
pluginId: PLUGIN_ID,
|
|
261
|
+
dataDir,
|
|
262
|
+
automationsDir: options.automationsDir || '',
|
|
263
|
+
scriptsDir: options.scriptsDir || '',
|
|
264
|
+
traceCount: options.traceCount,
|
|
265
|
+
scriptTimeoutSeconds: options.scriptTimeoutSeconds,
|
|
266
|
+
put: putPath,
|
|
267
|
+
notify,
|
|
268
|
+
log: { debug: app.debug ? app.debug.bind(app) : () => {}, error: app.error ? app.error.bind(app) : console.error }
|
|
269
|
+
})
|
|
270
|
+
runtime.load()
|
|
271
|
+
|
|
272
|
+
for (const [id, value] of Object.entries(runtime.helperValues)) {
|
|
273
|
+
emitHelper(id, value)
|
|
274
|
+
if (typeof app.registerPutHandler === 'function') {
|
|
275
|
+
app.registerPutHandler('vessels.self', helpers.helperPath(id), (context, p, v, cb) => {
|
|
276
|
+
try {
|
|
277
|
+
const next = runtime.setHelper(id, v)
|
|
278
|
+
emitHelper(id, next)
|
|
279
|
+
if (cb) cb({ state: 'COMPLETED' })
|
|
280
|
+
} catch (err) {
|
|
281
|
+
if (cb) cb({ state: 'COMPLETED', statusCode: 400, message: err.message })
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const paths = require('../lib/engine').collectPaths(runtime.doc)
|
|
288
|
+
if (paths.length && app.subscriptionmanager) {
|
|
289
|
+
app.subscriptionmanager.subscribe(
|
|
290
|
+
{
|
|
291
|
+
context: 'vessels.self',
|
|
292
|
+
subscribe: paths.map((p) => ({ path: p }))
|
|
293
|
+
},
|
|
294
|
+
unsubscribes,
|
|
295
|
+
(err) => app.error && app.error(err),
|
|
296
|
+
(delta) => {
|
|
297
|
+
const src = delta && delta.updates && delta.updates[0] && delta.updates[0].$source
|
|
298
|
+
;(delta.updates || []).forEach((u) => {
|
|
299
|
+
const source = u.$source || src
|
|
300
|
+
;(u.values || []).forEach((v) => {
|
|
301
|
+
runtime.handlePathChange(v.path, v.value, source).catch((err) => {
|
|
302
|
+
if (app.error) app.error(err)
|
|
303
|
+
})
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
timers.push(setInterval(() => {
|
|
311
|
+
runtime.tickSchedule().catch((err) => app.error && app.error(err))
|
|
312
|
+
}, 15000))
|
|
313
|
+
|
|
314
|
+
if (app.setPluginStatus) {
|
|
315
|
+
const n = runtime.doc.automations.length
|
|
316
|
+
const err = runtime.doc.errors.length
|
|
317
|
+
app.setPluginStatus(n + ' automations' + (err ? ', ' + err + ' YAML error(s)' : ''))
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
plugin.stop = function () {
|
|
322
|
+
timers.splice(0).forEach((t) => clearInterval(t))
|
|
323
|
+
unsubscribes.splice(0).forEach((f) => {
|
|
324
|
+
try {
|
|
325
|
+
f()
|
|
326
|
+
} catch (_) {}
|
|
327
|
+
})
|
|
328
|
+
runtime = null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return plugin
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function fsMkdir (dir) {
|
|
335
|
+
const fs = require('fs')
|
|
336
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports.app = 'app'
|
|
340
|
+
module.exports.options = 'options'
|