signalk-barograph
Version:
SignalK plugin to influx environment data & Barograph to visualize atmospheric pressure
356 lines (322 loc) • 14.5 kB
JavaScript
/*
Copyright © 2025 Inspired Technologies. Rights Reserved.
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 debug = require("debug")("signalk:signalk-barograph")
const { getSourceId } = require('@signalk/signalk-schema')
const { DateTime } = require('luxon');
const influx = require("./influx")
const barometer = require("./barometer")
const appconfig = require ("./appconfig")
const convert = require("./skunits")
const fs = require('fs')
const path = require('path')
const WAITING = 'waiting ...'
module.exports = function (app) {
var plugin = {};
var influxConfig = {
initialized: false
};
plugin.id = 'signalk-barograph';
plugin.name = 'Barograph powered by SignalK';
plugin.description = 'Plugin to provide data & graphics for aggregated environmental information using influxdb (pressure, temperature, humidity)';
var unsubscribes = [];
let timerId;
let metrics;
let pathConfig = {}
let valueConfig = {}
function saveconfig(dir, file, content) {
fs.writeFileSync(
path.join(dir, file),
JSON.stringify(content).concat("\n"), (err) => {
if (err) throw err;
return null
}
)
return file
}
function reconfig(path, config) {
if (config.includes('|>')) {
// replace
const param = config.split('|>')
return path.replace(param[0], param[1])
}
return null
}
function source(update) {
if (update['$source']) {
return update['$source']
} else if (update['source']) {
return getSourceId(update['source'])
}
return ''
}
function subscribe(influxDB, result) {
if (influxConfig.initialized && result.status === 'pass' ) {
if (influxConfig.paths.length===0) {
// Reconfig subscriptions
influxConfig.paths = influx.config('environment', 10*1000)
influx.config('navigation', 0).forEach(p => influxConfig.paths.push(p))
var options = app.readPluginOptions();
saveconfig(app.getDataDirPath(), options.configuration.pathConfig, influxConfig.paths)
}
if (influxConfig.paths.length>0)
{
influxConfig.paths.forEach(p => {
if (p.hasOwnProperty('config'))
pathConfig[p.path] = reconfig(p.path, p.config)
if (p.hasOwnProperty('convert'))
valueConfig[p.path] = p.convert
if (p.hasOwnProperty('trend')) {
barometer.addSubscriptionHandler(p.trend, p.path)
appconfig.addSubscription(p.trend, p.path)
// hack: server version > 1.39, startup timing issue
let val = app.getSelfPath(p.path)
if (p.trend==='altitude' && val && val.value) {
barometer.onElevationUpdate(val.value)
}
}
});
}
let preload = barometer.preLoad()
if (preload)
{
sendDelta(preload.update)
if (preload.meta!==null)
sendMeta(preload.meta)
}
app.setPluginStatus('Initialized');
timerId = setInterval(() => {
app.debug (`Sending ${metrics.length} data points to be uploaded to influx`)
if (metrics.length !== 0) {
influx.post(influxDB, metrics, influxConfig, log)
influx.buffer(metrics)
metrics = []
}
let updates = barometer.getTrendAndPredictions(2*influxConfig.loadFrequency*1000)
if (updates.length>0)
sendDelta(updates)
}, influxConfig.loadFrequency*1000)
app.debug (`Interval started, upload frequency: ${influxConfig.loadFrequency}s`)
var appConfigTimer = setInterval( (log) => {
appconfig.setAppUserData(log);
clearInterval(appConfigTimer);
}, 5000, log);
let localSubscription = {
context: 'vessels.self', // Get data only for self context
subscribe: influxConfig.paths
};
app.subscriptionmanager.subscribe (
localSubscription,
unsubscribes,
subscriptionError => {
app.error('Error:' + subscriptionError);
},
delta => {
if (!delta.updates) { return; }
delta.updates.forEach(u => {
if (!u.values || u.values[0].path===undefined || (u.values[0].value===WAITING || u.values[0].value===null ||
(typeof u.values[0].value==="object" && Object.keys(u.values[0].value)===0))) {
return;
}
const path = (pathConfig[u.values[0].path] ? pathConfig[u.values[0].path] : u.values[0].path)
const values = !valueConfig[u.values[0].path] ? u.values[0].value :
convert.toTarget(valueConfig[u.values[0].path].split('|>')[0], u.values[0].value, valueConfig[u.values[0].path].split('|>')[1]).value
var timestamp = DateTime.fromISO(u.timestamp)
if (path==='environment.forecast.time')
// conversion not required due to dt format change in openweather plugin (v0.5)
influxConfig.currentForecast = DateTime.fromISO(u.values[0].value)
else {
if (path.includes('environment.forecast')) {
let fctime = app.getSelfPath('environment.forecast.time')
if (!fctime && !influxConfig.currentForecast)
; // do nothing
else if (!fctime || fctime.value===null || fctime.value===WAITING)
timestamp = influxConfig.currentForecast.toUTC()
else
timestamp = DateTime.fromISO(fctime.value).toUTC()
}
else if (barometer.isSubscribed(u.values[0].path))
{ // fix: subscription is on the original path
barometer.onDeltaUpdate(u)
}
if (path.includes('environment'))
{
const metric = influx.format(path, values, timestamp, source(u))
if (metric!==null)
metrics.push(metric)
}
}
})
}
);
app.setPluginStatus('Started');
app.debug('Plugin started');
return true
}
else
{
app.setPluginError('Failed to connect to Influx');
return false
}
}
plugin.start = function (options, restartPlugin) {
app.debug('Plugin starting ...');
app.setPluginStatus('Initializing');
metrics = []
influx.cacheBuffer = []
influxConfig.cacheDir = app.getDataDirPath()
var configFile = options.pathConfig
if (options.pathConfig===undefined)
{
options.pathConfig = 'pathconfig.json'
configFile = saveconfig(app.getDataDirPath(), options.pathConfig, [])
app.savePluginOptions(options, () => {app.debug('Plugin options saved')});
}
try {
influxConfig.paths = require(configFile.includes('/') ? configFile : require('path').join(app.getDataDirPath(), configFile))
}
catch {
let paths = influx.config('environment', 10*1000)
influx.config('navigation', 0).forEach(p => paths.push(p))
configFile = saveconfig(app.getDataDirPath(), options.pathConfig, paths)
influxConfig.paths = require(configFile.includes('/') ? configFile : require('path').join(app.getDataDirPath(), configFile))
}
influxConfig.organization = (options.influxOrg ? options.influxOrg : '')
influxConfig.write = (options.influxBucket ? options.influxBucket : '')
influxConfig.read = (options.influxRead ? options.influxRead : (options.influxBucket ? options.influxBucket : ''))
influxConfig.retention = (options.writeRetention ? options.writeRetention : 3)
influxConfig.id = app.getSelfPath('mmsi') ? app.getSelfPath('mmsi') : app.getSelfPath('uuid')
appconfig.addInflux('id', influxConfig.id)
const influxDB = influx.login({
url: options.influxUri, // get from options
token: options.influxToken, // get from options
timeout: 10 * 1000 // 10sec timeout for health check
}, log)
appconfig.addInflux('url', options.influxUri+(influxConfig.organization==='' ? '/api/v2/query' : ''))
appconfig.addInflux('token', options.influxToken)
appconfig.addInflux('org', influxConfig.organization)
appconfig.addInflux('write', influxConfig.write)
appconfig.addInflux('read', influxConfig.read)
appconfig.addInflux('retention', influxConfig.retention)
appconfig.addInflux('username', options.influxToken.includes(':') ? options.influxToken.split(':')[0] : '') // not relevant for >2.x
appconfig.addInflux('password', options.influxToken.includes(':') ? options.influxToken.split(':')[1] : '') // not relevant for >2.x
let connectionString = []
if (!options.selfRef) {
connectionString.push('http://localhost:3000'); // default local server
connectionString.push(''); // empty username
connectionString.push(''); // empty password
} else if (connectionString.length===1) { // server only
connectionString.push(''); // empty username
connectionString.push(''); // empty password
} else {
connectionString=options.selfRef.split('|')
}
appconfig.init(connectionString[0], connectionString[1], connectionString[2], log)
influxConfig.initialized = influx.health(influxDB, log, subscribe)
influxConfig.loadFrequency = (options.loadFrequency ? options.loadFrequency : 30)
// TODO: if configured
barometer.init(app.debug, options["barometer"], influxConfig.loadFrequency)
app.debug('Plugin initialized');
};
plugin.stop = function () {
unsubscribes.forEach(f => f());
unsubscribes = [];
clearInterval(timerId)
app.debug('Interval Timer Stopped')
app.debug('Plugin stopped');
};
plugin.schema = {
type: 'object',
required: [
'influxUri',
'influxToken',
'influxBucket',
'pathConfig',
'loadFrequency'
],
properties: {
"influxUri": {
type: 'string',
title: 'InfluxDB URI'
},
"influxToken": {
type: 'string',
title: 'InfluxDB Token',
description: 'v2.x: [token]; V1.11.x: [username:password]'
},
"influxOrg": {
type: 'string',
title: 'InfluxDB Organisation',
description: 'v2.x: [required]; V1.11.x: [empty]'
},
"influxBucket": {
type: 'string',
title: 'InfluxDB Write Bucket',
description: 'v2.x: [bucket]; v1.11.x: [database/retentionpolicy]'
},
"influxRead": {
type: 'string',
title: 'InfluxDB Read Bucket',
description: 'v2.x: [bucket]; v1.11.x: [database/retentionpolicy]',
},
"selfRef": {
type: 'string',
title: 'Connection String (server|username|password)',
default: 'http://localhost:3000|user|pwd',
description: 'SignalK Server credentials required to provide configuration to embedded web app'
},
"pathConfig": {
type: 'string',
title: 'Paths Configuration',
description: 'paths config file [plugin data directory]',
default: 'barograph.config.json'
},
"loadFrequency": {
type: 'number',
title: 'Write Interval',
description: 'frequency of batched write to InfluxDB in s',
default: 30
},
"writeRetention": {
type: 'number',
title: 'Write Bucket Retention',
description: 'if entered (hours), read bucket will only be used for queries greater than write retention period',
default: 3
},
}
};
/**
*
* @param {Array<[{path:path, value:value}]>} messages
*/
function sendDelta(messages) {
app.handleMessage('signalk-barograph', {
updates: [
{
values: messages
}
]
});
}
function sendMeta(units) {
app.handleMessage('signalk-barograph', {
updates: [
{
meta: units
}
]
})
}
function log(msg) { app.debug(msg); }
return plugin;
};