@signalk/signalk-node-red
Version:
Combine Node-RED with Signal K data
243 lines (215 loc) • 6.76 kB
JavaScript
/**
* Copyright 2018 Scott Bender (scott@scottbender.net)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const RED = require("node-red")
const compareVersions = require('compare-versions')
const _ = require('lodash')
module.exports = function(app) {
var plugin = {};
var unsubscribes = []
var options
var redSettings
var supportsSecurity = compareVersions(app.config.version, '1.3.0') > 0
var deltaHandlers = []
var didRegisterInputHandler = false
plugin.id = "signalk-node-red";
plugin.name = "Node Red";
plugin.description = "Embeds node red in signalk node server";
plugin.start = function(theOptions) {
redSettings = {
userDir: app.config.configPath + '/red',
logging: {'console': { level: theOptions.logging || 'info'}},
credentialSecret: 'jkhdfshjkfdskjhfsjfsdkjhsfdjkfsd',
functionGlobalContext: {
streambundle: app.streambundle,
signalk: app.signalk,
subscriptionmanager: app.subscriptionmanager,
app: app,
plugin: plugin,
lodash: require('lodash'),
geodist: require('geodist')
}
};
if ( theOptions.flowFile && theOptions.flowFile.length > 0 ) {
redSettings.flowFile = theOptions.flowFile
}
if ( theOptions.settings ) {
theOptions.settings.forEach(s => {
try {
app.debug('setting %s to %s', s.name, s.value)
redSettings[s.name] = JSON.parse(s.value)
} catch ( e ) {
app.error(`unable to parse setting ${s.name} of ${s.value}`)
app.setPluginError(`unable to parse setting ${s.name} of ${s.value}`)
}
})
}
if ( theOptions.requires ) {
theOptions.requires.forEach(module => {
try {
redSettings.functionGlobalContext[module] = require(module)
} catch (err) {
app.error(`unable to load module ${module} ${err}`)
}
})
}
if ( !supportsSecurity ) {
redSettings.httpAdminRoot = '/redAdmin'
redSettings.httpNodeRoot = '/redApi'
} else {
redSettings.httpAdminRoot = '/plugins/' + plugin.id + '/redAdmin'
redSettings.httpNodeRoot = '/plugins/' + plugin.id + '/redApi'
}
RED.init(app.server, redSettings)
RED.start()
if ( !supportsSecurity ) {
app.use(redSettings.httpAdminRoot, RED.httpAdmin);
app.use(redSettings.httpNodeRoot, RED.httpNode);
} else {
app.get('/redAdmin', (req, res) => {
res.redirect(redSettings.httpAdminRoot)
})
app.use(redSettings.httpAdminRoot, RED.httpNode);
}
unsubscribes.push(RED.stop)
}
plugin.getSelfPath = function(path, timeout) {
let res = app.getSelfPath(path)
if ( res && !_.isUndefined(res.value) && !_.isUndefined(res.timestamp)
&& Date.now() - new Date(res.timestamp).getTime() < timeout ) {
return res.value
}
return undefined
}
function deltaInputHandler(delta, next) {
if ( deltaHandlers.length == 0 ) {
next(delta)
return
}
if ( delta.updates ) {
delta.updates.forEach(update => {
if ( update.values ) {
let newValues = []
update.values.forEach(pathValue => {
let matched = false
deltaHandlers.forEach(handler => {
if ( delta.context == handler.context
&& pathValue.path == handler.path
&& (!handler.source
|| (update.$source && handler.source == update.$source)) ) {
matched = true
handler.callback({
topic: pathValue.path,
payload: pathValue.value,
$source: update.$source,
source: update.source,
context: delta.context,
timestamp: update.timestamp
}, next)
}
})
if ( !matched ) {
newValues.push(pathValue)
}
})
update.values = newValues
}
})
}
next(delta)
}
plugin.registerDeltaInputHandler = function(context, path, source, cb) {
const info = {
context: context === 'vessels.self' ? app.selfContext : context,
path: path,
source: source,
callback: cb
}
deltaHandlers.push(info)
if ( !this.didRegisterInputHandler ) {
app.registerDeltaInputHandler(deltaInputHandler)
this.didRegisterInputHandler = true
}
return () => {
let idx = deltaHandlers.indexOf(info)
deltaHandlers.splice(idx, 1)
}
}
plugin.registerWithRouter = function(router) {
if ( supportsSecurity ) {
router.use('/redAdmin', (req, res, next) => {
if ( RED.httpAdmin ) {
RED.httpAdmin(req, res, next)
} else {
res.status(404).send(`The ${plugin.id} plugin is not started`)
}
});
router.use('/redApi', (req, res, next) => {
if ( RED.httpNode ) {
RED.httpNode(req, res, next)
} else {
res.status(404).send(`The ${plugin.id} plugin is not started`)
}
});
}
}
plugin.stop = function() {
app.debug("stopping...")
unsubscribes.forEach(f => f());
unsubscribes = [];
didRegisterInputHandler = false
};
plugin.schema = {
title: "Node Red",
properties: {
flowFile: {
type: 'string',
title: 'Flow File',
description: 'the file used to store the flows'
},
logging: {
type: 'string',
title: 'Logging',
enum: [ 'falal', 'error', 'warn', 'info', 'debug', 'trace' ],
default: 'info'
},
requires: {
type: 'array',
title: 'Npm modules to make available',
items: {
type: 'string'
}
},
settings: {
title: 'Node Red Settings',
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
title: 'Setting Name'
},
value: {
type: 'string',
title: 'Setting Value (json)'
}
}
}
}
}
}
return plugin;
}