UNPKG

@signalk/freeboard-sk

Version:

Openlayers chart plotter implementation for Signal K

414 lines (413 loc) 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getWeather = exports.listWeather = exports.stopWeather = exports.initWeather = exports.WEATHER_POLL_INTERVAL = exports.defaultStationId = void 0; // **** Experiment: OpenWeather integration **** const server_api_1 = require("@signalk/server-api"); const openweather_1 = require("./openweather"); // default weather station context exports.defaultStationId = `freeboard-sk`; let server; let pluginId; const wakeInterval = 60000; let lastWake; // last wake time let lastFetch; // last successful fetch let fetchInterval = 3600000; // 1hr // eslint-disable-next-line @typescript-eslint/no-explicit-any let timer; const errorCountMax = 5; // max number of consecutive errors before terminating timer let errorCount = 0; // number of consecutive fetch errors (no position / failed api connection, etc) let weatherData; let weatherService; let weatherServiceName; exports.WEATHER_POLL_INTERVAL = [60, 30, 15]; const initWeather = (app, id, config) => { server = app; pluginId = id; fetchInterval = (config.pollInterval ?? 60) * 60000; if (isNaN(fetchInterval)) { fetchInterval = 60 * 60000; } server.debug(`*** Weather: settings: ${JSON.stringify(config)}`); server.debug(`*** fetchInterval: ${fetchInterval}`); weatherService = new openweather_1.OpenWeather(config); weatherServiceName = 'openweather'; initMeteoEndpoints(); if (!timer) { server.debug(`*** Weather: startTimer..`); timer = setInterval(() => fetchWeatherData(), wakeInterval); } fetchWeatherData(); }; exports.initWeather = initWeather; /** Initialise API endpoints */ const initMeteoEndpoints = () => { const meteoPath = '/signalk/v2/api/meteo'; server.get(`${meteoPath}`, async (req, res) => { server.debug(`${req.method} ${meteoPath}`); const r = await (0, exports.listWeather)({}); res.status(200); res.json(r); }); server.get(`${meteoPath}/:id`, async (req, res) => { server.debug(`${req.method} ${meteoPath}/:id`); const r = weatherData && weatherData[req.params.id] ? weatherData[req.params.id] : {}; res.status(200); res.json(r); }); server.get(`${meteoPath}/:id/observations`, async (req, res) => { server.debug(`${req.method} ${meteoPath}/:id/observations`); const r = weatherData && weatherData[req.params.id] && weatherData[req.params.id].observations ? weatherData[req.params.id].observations : {}; res.status(200); res.json(r); }); server.get(`${meteoPath}/:id/observations/:index`, async (req, res) => { server.debug(`${req.method} ${meteoPath}/:id/observations/:index`); const r = weatherData && weatherData[req.params.id] && weatherData[req.params.id].observations && weatherData[req.params.id].observations[req.params.index] ? weatherData[req.params.id].observations[req.params.index] : {}; res.status(200); res.json(r); }); server.get(`${meteoPath}/:id/forecasts`, async (req, res) => { server.debug(`${req.method} ${meteoPath}/:id/forecasts`); const r = weatherData && weatherData[req.params.id] && weatherData[req.params.id].forecasts ? weatherData[req.params.id].forecasts : {}; res.status(200); res.json(r); }); server.get(`${meteoPath}/:id/forecasts/:index`, async (req, res) => { server.debug(`${req.method} ${meteoPath}/:id/forecasts/:index`); const r = weatherData && weatherData[req.params.id] && weatherData[req.params.id].forecasts && weatherData[req.params.id].forecasts[req.params.index] ? weatherData[req.params.id].forecasts[req.params.index] : {}; res.status(200); res.json(r); }); server.get(`${meteoPath}/:id/warnings`, async (req, res) => { server.debug(`${req.method} ${meteoPath}/:id/warnings`); const r = weatherData && weatherData[req.params.id] && weatherData[req.params.id].warnings ? weatherData[req.params.id].warnings : {}; res.status(200); res.json(r); }); server.get(`${meteoPath}/:id/warnings/:index`, async (req, res) => { server.debug(`${req.method} ${meteoPath}/:id/warnings/:index`); const r = weatherData && weatherData[req.params.id] && weatherData[req.params.id].warnings && weatherData[req.params.id].warnings[req.params.index] ? weatherData[req.params.id].warnings[req.params.index] : {}; res.status(200); res.json(r); }); }; /** stop weather service */ const stopWeather = () => { stopTimer(); lastFetch = fetchInterval - 1; }; exports.stopWeather = stopWeather; /** stop interval timer */ const stopTimer = () => { if (timer) { server.debug(`*** Weather: Stopping timer.`); clearInterval(timer); } }; /** * Handle fetch errors * @param msg mesgage to log */ const handleError = (msg) => { console.log(msg); errorCount++; if (errorCount >= errorCountMax) { // max retries exceeded.... going to sleep console.log(`*** Weather: Failed to fetch data after ${errorCountMax} attempts.\nRestart ${pluginId} plugin to retry.`); stopTimer(); } else { console.log(`*** Weather: Error count = ${errorCount} of ${errorCountMax}`); console.log(`*** Retry in ${wakeInterval / 1000} seconds.`); } }; /** Fetch weather data from provider */ const fetchWeatherData = () => { server.debug(`*** Weather: fetchWeatherData()`); // runaway check if (lastWake) { const dt = Date.now() - lastWake; const flagValue = wakeInterval - 10000; if (dt < flagValue) { server.debug(`Watchdog -> Awake!...(${dt / 1000} secs)... stopping timer...`); stopTimer(); server.setPluginError('Weather timer stopped by watchdog!'); return; } } lastWake = Date.now(); // eslint-disable-next-line @typescript-eslint/no-explicit-any const pos = server.getSelfPath('navigation.position'); if (!pos) { handleError(`*** Weather: No vessel position detected!`); return; } server.debug(`*** Vessel position: ${JSON.stringify(pos.value)}.`); // check if fetchInterval has lapsed if (lastFetch) { const e = Date.now() - lastFetch; if (e < fetchInterval) { server.debug(`*** Weather: Next poll due in ${Math.round((fetchInterval - e) / 60000)} min(s)... sleeping for ${wakeInterval / 1000} seconds...`); return; } } if (errorCount < errorCountMax) { server.debug(`*** Weather: Calling service API.....`); server.debug(`Position: ${JSON.stringify(pos.value)}`); server.debug(`*** Weather: polling weather provider.`); weatherService .fetchData(pos.value) .then((data) => { server.debug(`*** Weather: data received....`); server.debug(JSON.stringify(data)); errorCount = 0; lastFetch = Date.now(); lastWake = Date.now(); weatherData = data; emitMeteoDeltas(); checkForWarnings(); }) .catch((err) => { handleError(`*** Weather: ERROR polling weather provider!`); console.log(err.message); server.setPluginError(err.message); }); } }; // eslint-disable-next-line @typescript-eslint/no-explicit-any const listWeather = async (params) => { server.debug(`getWeather ${JSON.stringify(params)}`); const res = {}; if (weatherData) { for (const o in weatherData) { const { id, name, position } = weatherData[o]; res[o] = { id, name, position }; } } return res; }; exports.listWeather = listWeather; const getWeather = async (path, property // eslint-disable-next-line @typescript-eslint/no-explicit-any ) => { server.debug(`getWeather ${path}, ${property}`); if (!weatherData) { return {}; } const station = weatherData[path]; if (!station) { throw `Weather station ${path} not found!`; } if (property) { const value = property.split('.').reduce((acc, val) => { return acc[val]; }, station); return value ?? {}; } else { return station; } }; exports.getWeather = getWeather; // check for weather warnings in returned data const checkForWarnings = () => { if ('defaultStationId' in weatherData) { if (weatherData[exports.defaultStationId].warnings && Array.isArray(weatherData[exports.defaultStationId].warnings)) { server.debug(`*** No. Warnings ${weatherData[exports.defaultStationId].warnings.length}`); if (weatherData[exports.defaultStationId].warnings.length !== 0) { emitWarningNotification(weatherData[exports.defaultStationId].warnings[0]); } else { emitWarningNotification(); } } else { emitWarningNotification(); } } }; // emit weather warning notification const emitWarningNotification = (warning) => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let delta; if (warning) { server.debug(`** Setting Notification **`); server.debug(JSON.stringify(warning)); delta = { path: 'notifications.meteo.warning', value: { state: server_api_1.ALARM_STATE.warn, method: [server_api_1.ALARM_METHOD.visual], message: warning.details ? warning.details : warning.type ?? warning.source } }; } else { server.debug(`** Clearing Notification **`); delta = { path: 'notifications.meteo.warning', value: { state: server_api_1.ALARM_STATE.normal, method: [], message: '' } }; } server.handleMessage(pluginId, { context: `meteo.${exports.defaultStationId}`, updates: [{ values: [delta] }] }, server_api_1.SKVersion.v2); }; // Meteo methods const emitMeteoDeltas = () => { const pathRoot = 'environment'; const deltaValues = []; server.debug('**** METEO - emit deltas*****'); if (weatherData) { deltaValues.push({ path: 'navigation.position', value: weatherData[exports.defaultStationId].position }); const obs = weatherData[exports.defaultStationId].observations; server.debug('**** METEO *****'); if (obs && Array.isArray(obs)) { server.debug('**** METEO OBS *****'); obs.forEach((o) => { deltaValues.push({ path: ``, value: { name: weatherServiceName } }); if (typeof o.date !== 'undefined') { deltaValues.push({ path: `${pathRoot}.date`, value: o.date }); } if (typeof o.outside.horizontalVisibility !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.horizontalVisibility`, value: o.outside.horizontalVisibility }); } if (typeof o.sun.sunrise !== 'undefined') { deltaValues.push({ path: `${pathRoot}.sun.sunrise`, value: o.sun.sunrise }); } if (typeof o.sun.sunset !== 'undefined') { deltaValues.push({ path: `${pathRoot}.sun.sunset`, value: o.sun.sunset }); } if (typeof o.outside.uvIndex !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.uvIndex`, value: o.outside.uvIndex }); } if (typeof o.outside.cloudCover !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.cloudCover`, value: o.outside.cloudCover }); } if (typeof o.outside.temperature !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.temperature`, value: o.outside.temperature }); } if (typeof o.outside.dewPointTemperature !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.dewPointTemperature`, value: o.outside.dewPointTemperature }); } if (typeof o.outside.feelsLikeTemperature !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.feelsLikeTemperature`, value: o.outside.feelsLikeTemperature }); } if (typeof o.outside.pressure !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.pressure`, value: o.outside.pressure }); } if (typeof o.outside.relativeHumidity !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.relativeHumidity`, value: o.outside.relativeHumidity }); } if (typeof o.outside.absoluteHumidity !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.absoluteHumidity`, value: o.outside.absoluteHumidity }); } if (typeof o.outside.precipitationType !== 'undefined') { deltaValues.push({ path: `${pathRoot}.outside.precipitationType`, value: o.outside.precipitationType }); } if (typeof o.wind.speedTrue !== 'undefined') { deltaValues.push({ path: `${pathRoot}.wind.speedTrue`, value: o.wind.speedTrue }); } if (typeof o.wind.directionTrue !== 'undefined') { deltaValues.push({ path: `${pathRoot}.wind.directionTrue`, value: o.wind.directionTrue }); } }); // eslint-disable-next-line @typescript-eslint/no-explicit-any const updates = { values: deltaValues }; server.handleMessage(pluginId, { context: `meteo.${exports.defaultStationId}`, updates: [updates] }, server_api_1.SKVersion.v1); } } };