UNPKG

signalk-to-mongodb

Version:

SignalK plugin to store data to cloud hosted MongoDB URI. It provides an easy integration of SignalK server with MongoDB to log and store boat data in real-time.

277 lines (256 loc) 9.48 kB
const { MongoDb } = require('./mongodb'); /** * @fileoverview This file contains a SignalK plugin that sends data to MongoDB. * For more information, please refer to the Signalk plugin documentation: * {@link https://demo.signalk.org/documentation/develop/plugins/server_plugin.html} */ ``; /** * Factory function to create a SignalK plugin that sends data to MongoDB. * @param {Object} app - The SignalK server application instance. * @returns {Object} The plugin object with start and stop capabilities. */ module.exports = function (app) { let plugin = {}; let options = null; let mongodb = null; let unsubscribes = []; let selfContext; /** * Determines the 'self' context from the SignalK server settings. * @returns {string|null} The self context string or null if not found. */ let getSelfContext = function () { const selfUuid = app.getSelfPath('uuid'); const selfMmsi = app.getSelfPath('mmsi'); if (selfUuid) { return 'vessels.' + selfUuid; } else if (selfMmsi) { return 'vessels.urn:mrn:imo:mmsi:' + selfMmsi.toString(); } return null; }; /** * Handles incoming data updates from SignalK paths and forwards them to MongoDB. * @param {Object} delta - The SignalK delta message containing updates. * @param {Object} pathOption - Configuration for specific SignalK path handling. */ plugin.handleUpdates = function (delta, pathOption) { app.debug(`handleUpdates delta: ${JSON.stringify(delta)}`); app.debug(`handleUpdates pathOption: ${JSON.stringify(pathOption)}`); delta.updates.forEach(update => { app.debug(`handleUpdates update: ${JSON.stringify(update)}`); if (!update.values) { return; } update.values.forEach(val => { try { let payload = { source: update['$source'], context: delta.context, path: val.path, time: new Date(update.timestamp), // Ensure time is stored as Date }; // Determine the type of value and handle accordingly if (val.path === 'navigation.position') { // Ensure GeoJSON format for navigation.position if (typeof val.value === 'object' && val.value.latitude && val.value.longitude) { payload.value = { type: 'Point', coordinates: [val.value.longitude, val.value.latitude], }; } else { // Handle invalid or unexpected structures app.error(`Invalid position value: ${JSON.stringify(val.value)}`); return; } } else if (typeof val.value === 'number' || typeof val.value === 'string') { // Store numbers and strings directly payload.value = val.value; } else if (typeof val.value === 'object') { // Store JSON objects directly payload.value = val.value; } else { // Handle unexpected types (e.g., undefined, NaN) app.error(`Unexpected value type: ${typeof val.value} for path: ${val.path}`); return; } options.defaultTags.forEach(tag => { payload[tag.name] = tag.value; }); pathOption.pathTags.forEach(tag => { payload[tag.name] = tag.value; }); if (options.tagAsSelf && delta.context.localeCompare(selfContext) === 0) { payload['self'] = true; } app.debug(`handleUpdates sending payload: ${JSON.stringify(payload)}`); mongodb.send(payload); } catch (error) { app.error(`Skipping update due to error: ${JSON.stringify(val)}, error: ${error.message}`); } }); }); }; /** * Starts the plugin, setting up MongoDB connection and subscriptions to SignalK paths. * @param {Object} opts - Configuration options for the plugin. * @param {Function} restart - Callback to restart the plugin with new settings. */ plugin.start = function (opts, restart) { app.debug('Plugin started'); options = opts; selfContext = getSelfContext(); app.debug(`Self context: ${selfContext}`); mongodb = new MongoDb(app, options.dbUri, options.database, options.collection); mongodb.start(options); options.pathArray.forEach(pathOption => { app.debug('Configuring pathOption: ' + JSON.stringify(pathOption)); if (pathOption.enabled) { let localSubscription = { context: pathOption.context, subscribe: [ { path: pathOption.path, policy: 'instant', minPeriod: pathOption.interval, }, ], }; app.subscriptionmanager.subscribe( localSubscription, unsubscribes, subscriptionError => { app.error(`Subscription error: ${subscriptionError}`); }, delta => { this.handleUpdates(delta, pathOption); } ); app.debug(`Added subscription for: ${JSON.stringify(localSubscription)}`); } else { app.error(`Skipping subscription for: ${pathOption.context}/.../${pathOption.path}`); } }); }; /** * Stops the plugin, unsubscribing from all paths and closing MongoDB connection. */ plugin.stop = function () { unsubscribes.forEach(f => f()); unsubscribes = []; if (mongodb) { mongodb.stop(); app.debug('MongoDB connection stopped'); } app.debug('Plugin stopped'); }; // Plugin metadata and schema for configuration plugin.id = 'signalk-to-mongodb'; plugin.name = 'SignalK to MongoDB Plugin'; plugin.description = 'This plugin sends SignalK data updates to a configured MongoDB instance.'; // Plugin configuration schema plugin.schema = { type: 'object', properties: { dbUri: { type: 'string', title: 'MongoDB URI', description: 'The URI to connect to your MongoDB instance' }, database: { type: 'string', title: 'Database Name', description: 'The name of the MongoDB database to use' }, collection: { type: 'string', title: 'Collection Name', description: 'The name of the MongoDB collection to use', }, batchSize: { type: 'number', title: 'Batch Size', default: 100, description: 'Number of values to send in a single batch to the MongoDB endpoint', }, flushSecs: { type: 'number', title: 'Flush Interval', default: 60, description: "Maximum time in seconds to keep points in an unflushed batch, 0 means don't periodically flush", }, maxBuffer: { type: 'number', title: 'Maximum Buffer Size', default: 1000, description: 'Maximum size of the buffer - it contains items that could not be sent for the first time', }, ttlSecs: { type: 'number', title: 'Maximum Time to Live', default: 180, description: 'Maximum time to buffer data in seconds - older data is automatically removed from the buffer', }, tagAsSelf: { type: 'boolean', title: "Tag as 'self' if applicable", default: true, description: 'Tag measurements as {self: true} when from vessel.self - requires an MMSI or UUID to be set in the Vessel Base Data on the Server->Settings page', }, defaultTags: { type: 'array', title: 'Default Tags', default: [], description: 'Default tags added to every measurement', items: { type: 'object', properties: { name: { type: 'string', title: 'Tag Name' }, value: { type: 'string', title: 'Tag Value' }, }, required: ['name', 'value'], }, }, pathArray: { type: 'array', title: 'Paths', default: [], description: 'Configure paths for data recording', items: { type: 'object', properties: { enabled: { type: 'boolean', title: 'Enabled', default: true, description: 'Enable writes to MongoDB for this path', }, context: { type: 'string', title: 'SignalK context', description: "Context to record, e.g., 'self' for own ship, 'vessels.*' for all vessels", }, path: { type: 'string', title: 'SignalK path', description: "Path to record, e.g., 'navigation.position'" }, interval: { type: 'number', title: 'Recording interval', default: 1000, description: 'Minimum milliseconds between data records', }, pathTags: { type: 'array', title: 'Path Tags', default: [], description: 'Define any tags to include for this path', items: { type: 'object', properties: { name: { type: 'string', title: 'Tag Name' }, value: { type: 'string', title: 'Tag Value' }, }, required: ['name', 'value'], }, }, }, required: ['context', 'path', 'interval'], }, }, }, required: ['dbUri', 'database', 'collection'], }; return plugin; };