UNPKG

@marinminds/signalk-notification-publisher

Version:

Marinminds SignalK plugin that publishes data to an API when notification status changes

260 lines (226 loc) 8.85 kB
const axios = require('axios'); let lastStates = {}; // Object to keep track of the last known state for each notification path const KEYS = { position: 'navigation.position', state: 'navigation.state' }; const mediumTime = new Intl.DateTimeFormat("en", { timeStyle: "medium", dateStyle: "short", }); // Define the GraphQL mutation query const gqlQuery = ` mutation CreateEvent($yachtID: String!, $item: CreateEventInput!) { createEvent(yachtId: $yachtID, data: $item) { id title } } `; module.exports = function (app) { let unsubscribes = []; const plugin = { description: 'A SignalK plugin that publishes data directly to an API when notification status changes', id: 'marinminds-notification-publisher', name: 'Marinminds Notification Publisher' }; plugin.start = function (options) { if (!options.url) { app.setPluginError('GraphQL Plugin not started. GraphQL URL is not set.'); app.debug('GraphQL URL not set. Not started'); return; } app.setPluginStatus('GraphQL Plugin started. Waiting for first status update.'); const subscription = { context: 'vessels.self', subscribe: [{ path: 'notifications.*', period: 5000 }] }; app.subscriptionmanager.subscribe( subscription, unsubscribes, function () { app.error('Subscription error'); }, delta => { delta.updates.forEach(alarm => { handleAlarmUpdate(options, alarm); }); } ); }; plugin.stop = function () { unsubscribes.forEach(f => f()); unsubscribes = []; app.setPluginStatus('GraphQL Plugin stopped.'); }; plugin.schema = { type: 'object', required: ['url'], properties: { url: { type: "string", title: "URL of the API", }, apiKey: { type: "string", title: "API Key for authentication" }, yachtID: { type: "string", title: "Yacht ID", }, updateMoored: { type: "boolean", title: "Send updates when moored", default: true }, updatesFromServer: { type: "boolean", title: "Send updates from server", default: false } } }; function getKeyValue(key) { const data = app.getSelfPath(key); return data ? { value: data.value, unit: data.meta?.units } : null; } function handleAlarmUpdate(options, alarm) { app.debug(`Received alarm: ${JSON.stringify(alarm, null, 2)}`); const currentState = getKeyValue(KEYS.state); const data = { meta: { name: app.getSelfPath('name') }, position: getKeyValue(KEYS.position), state: currentState, yachtID: options.yachtID, alarm }; const ts = new Date(); const theTime = mediumTime.format(ts); // Condition to skip updates based on server notifications if (!options.updatesFromServer && alarm.values.some(value => value.path.startsWith('notifications.server'))) { app.setPluginStatus(`Skipping update for server notifications`); return; } // Condition to skip updates when moored if (!options.updateMoored && data.state?.value === "moored" && lastStates["state"] === "moored") { app.setPluginStatus(`Detected ship is moored, skipping update`); return; } let stateChanged = false; let alarmMessage; alarm.values.forEach(value => { const path = value.path; const newState = value.value.state; alarmMessage = value.value.message; // Retrieve the alarm message from the alarm values app.debug(`${path}: Current state: ${newState}, Last known state: ${lastStates[path]}`); // Check for the "undefined" to "nominal" or "undefined" to "normal" transition if ((lastStates[path] === undefined && newState === 'nominal') || (lastStates[path] === undefined && newState === 'normal')) { app.debug(`${path}: Skipping update due to state transition from "undefined" to "${newState}"`); lastStates[path] = newState; // Update the state to avoid this transition in the future return; } if (lastStates[path] !== newState) { app.debug(`${path}: State change detected. Updating last known state.`); lastStates[path] = newState; // Update the state for this path stateChanged = true; // Mark that a state change has occurred } }); // Send data to GraphQL if any state has changed if (stateChanged) { sendToGraphQL(options, data, theTime, alarmMessage); } else { app.debug(`State has not changed, skipping update`); } } function sendToGraphQL(options, data, theTime, alarmMessage) { // Enhanced validation and logging app.debug(`[sendToGraphQL] Starting GraphQL request at ${theTime}`); app.debug(`[sendToGraphQL] Configuration check:`); app.debug(` - URL: ${options.url || 'NOT SET'}`); app.debug(` - API Key: ${options.apiKey ? '***' + options.apiKey.slice(-4) : 'NOT SET'}`); app.debug(` - Yacht ID: ${options.yachtID || 'NOT SET'}`); app.debug(` - Alarm Message: ${alarmMessage || 'NOT SET'}`); // Validate required options if (!options.url) { app.setPluginError(`${theTime} : GraphQL URL not configured`); return; } if (!options.apiKey) { app.setPluginError(`${theTime} : API Key not configured`); return; } if (!options.yachtID) { app.setPluginError(`${theTime} : Yacht ID not configured`); return; } // Prepare the variables for the GraphQL mutation const variables = { yachtID: options.yachtID, item: { title: alarmMessage || 'Alarm Notification', // Use alarm message as the title type: 'Alarm', // Set the type as needed // Add more fields if your GraphQL schema supports them }, }; app.debug(`[sendToGraphQL] GraphQL Variables: ${JSON.stringify(variables, null, 2)}`); // Prepare the request config for Axios const config = { method: 'post', url: options.url, // Use the URL from the plugin options headers: { 'Content-Type': 'application/json', 'X-Api-Key': options.apiKey // Include the API key for authentication }, data: { query: gqlQuery, variables: variables, } }; app.debug(`[sendToGraphQL] Request config: ${JSON.stringify({ method: config.method, url: config.url, headers: { 'Content-Type': config.headers['Content-Type'], 'X-Api-Key': config.headers['X-Api-Key'] ? '***' + config.headers['X-Api-Key'].slice(-4) : 'NOT SET' }, dataKeys: Object.keys(config.data) }, null, 2)}`); // Send the request and handle the response axios.request(config) .then(response => { app.debug(`[sendToGraphQL] Success response status: ${response.status}`); app.debug(`[sendToGraphQL] Success response data: ${JSON.stringify(response.data, null, 2)}`); if (response.data.errors) { app.setPluginError(`${theTime} : GraphQL returned errors: ${JSON.stringify(response.data.errors, null, 2)}`); } else if (response.data.data && response.data.data.createEvent) { app.setPluginStatus(`${theTime} : Successfully sent event to GraphQL: ${response.data.data.createEvent.title}`); } else { app.setPluginError(`${theTime} : Unexpected GraphQL response structure: ${JSON.stringify(response.data, null, 2)}`); } }) .catch(error => { app.debug(`[sendToGraphQL] Error details:`); app.debug(` - Error message: ${error.message}`); app.debug(` - Error code: ${error.code || 'NO_CODE'}`); if (error.response) { app.debug(` - Response status: ${error.response.status}`); app.debug(` - Response statusText: ${error.response.statusText}`); app.debug(` - Response headers: ${JSON.stringify(error.response.headers, null, 2)}`); app.debug(` - Response data: ${JSON.stringify(error.response.data, null, 2)}`); app.setPluginError(`${theTime} : HTTP ${error.response.status} - ${error.response.statusText}: ${JSON.stringify(error.response.data, null, 2)}`); } else if (error.request) { app.debug(` - Request was made but no response received`); app.debug(` - Request details: ${JSON.stringify(error.request, null, 2)}`); app.setPluginError(`${theTime} : No response from GraphQL server. Check if server is running and URL is correct.`); } else { app.debug(` - Error setting up request: ${error.message}`); app.setPluginError(`${theTime} : Request setup error: ${error.message}`); } }); } return plugin; };