@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
JavaScript
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;
};