UNPKG

@signalk/freeboard-sk

Version:

Openlayers chart plotter implementation for Signal K

636 lines (635 loc) 22.2 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; // eslint-disable-next-line @typescript-eslint/no-explicit-any let retryTimer; const retryInterval = 10000; // time to wait after a failed api request const retryCountMax = 3; // max number of retries on failed api connection let retryCount = 0; // number of retries on failed api connection let noPosRetryCount = 0; // number of retries on no position detected let weatherData; let weatherService; let weatherServiceName; let metaSent = false; 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(); fetchWeatherData(); }; exports.initWeather = initWeather; 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); }); }; const stopWeather = () => { if (timer) { clearInterval(timer); } if (retryTimer) { clearTimeout(retryTimer); } lastFetch = fetchInterval - 1; }; exports.stopWeather = stopWeather; // 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; const fetchWeatherData = () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const pos = server.getSelfPath('navigation.position'); if (!pos) { // try <noPosRetryCount> of times to detect vessel position server.debug(`*** Weather: No vessel position detected!`); if (noPosRetryCount >= 3) { server.debug(`*** Weather: Maximum number of retries to detect vessel position!... sleeping.`); return; } noPosRetryCount++; retryTimer = setTimeout(() => { server.debug(`*** Weather: RETRY = ${noPosRetryCount} after no vessel position detected!`); fetchWeatherData(); }, 5000); return; } server.debug(`*** Vessel position: ${JSON.stringify(pos.value)}.`); noPosRetryCount = 0; if (retryTimer) { clearTimeout(retryTimer); } if (lastFetch) { const e = Date.now() - lastFetch; if (e < fetchInterval) { server.debug(`*** Weather: Next poll due in ${Math.round((fetchInterval - e) / 60000)} min(s)... sleep for ${wakeInterval / 1000} secs...`); return; } } if (retryCount < retryCountMax) { retryCount++; server.debug(`*** Weather: Calling service API.....(attempt: ${retryCount})`); 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)); retryCount = 0; lastFetch = Date.now(); lastWake = Date.now(); weatherData = data; timer = setInterval(() => { server.debug(`*** Weather: wake from sleep....poll provider.`); const dt = Date.now() - lastWake; // check for runaway timer if (dt >= 50000) { server.debug('Wake timer watchdog -> OK'); server.debug(`*** Weather: Polling provider.`); } else { server.debug('Wake timer watchdog -> NOT OK... Stopping wake timer!'); server.debug(`Watch interval < 50 secs. (${dt / 1000} secs)`); clearInterval(timer); server.setPluginError('Weather watch timer error!'); } lastWake = Date.now(); fetchWeatherData(); }, wakeInterval); emitMeteoDeltas(); checkForWarnings(); }) .catch((err) => { server.debug(`*** Weather: ERROR polling weather provider! (retry in ${retryInterval / 1000} sec)`); console.log(err.message); server.setPluginError(err.message); // sleep and retry retryTimer = setTimeout(() => fetchWeatherData(), retryInterval); }); } else { // max retries. sleep and retry? retryCount = 0; console.log(`*** Weather: Failed to fetch data after ${retryCountMax} attempts.\nRestart ${pluginId} plugin to retry.`); } }; // 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 }; if (!metaSent) { server.debug('**** SENDING METAS *****'); updates.meta = buildMeteoMetas(); metaSent = true; } server.handleMessage(pluginId, { context: `meteo.${exports.defaultStationId}`, updates: [updates] }, server_api_1.SKVersion.v1); } } }; const buildMeteoMetas = () => { server.debug('**** METEO - build metas *****'); let metas = []; metas = metas.concat(buildObservationMetas('environment')); return metas; }; const buildObservationMetas = (pathRoot) => { const metas = []; server.debug('**** METEO - building observation metas *****'); metas.push({ path: `${pathRoot}.date`, value: { description: 'Time of measurement.' } }); metas.push({ path: `${pathRoot}.sun.sunrise`, value: { description: 'Time of sunrise at the related position.' } }); metas.push({ path: `${pathRoot}.sun.sunset`, value: { description: 'Time of sunset at the related position.' } }); metas.push({ path: `${pathRoot}.outside.uvIndex`, value: { description: 'Level of UV radiation. 1 UVI = 25mW/sqm', units: 'UVI' } }); metas.push({ path: `${pathRoot}.outside.cloudCover`, value: { description: 'Cloud clover.', units: 'ratio' } }); metas.push({ path: `${pathRoot}.outside.temperature`, value: { description: 'Outside air temperature.', units: 'K' } }); metas.push({ path: `${pathRoot}.outside.dewPointTemperature`, value: { description: 'Dew point.', units: 'K' } }); metas.push({ path: `${pathRoot}.outside.feelsLikeTemperature`, value: { description: 'Feels like temperature.', units: 'K' } }); metas.push({ path: `${pathRoot}.outside.horizontalVisibility`, value: { description: 'Horizontal visibility.', units: 'm' } }); metas.push({ path: `${pathRoot}.outside.horizontalVisibilityOverRange`, value: { description: 'Visibilty distance is greater than the range of the measuring equipment.' } }); metas.push({ path: `${pathRoot}.outside.pressure`, value: { description: 'Barometric pressure.', units: 'Pa' } }); metas.push({ path: `${pathRoot}.outside.pressureTendency`, value: { description: 'Integer value indicating barometric pressure value tendency e.g. 0 = steady, etc.' } }); metas.push({ path: `${pathRoot}.outside.pressureTendencyType`, value: { description: 'Description for the value of pressureTendency e.g. steady, increasing, decreasing.' } }); metas.push({ path: `${pathRoot}.outside.relativeHumidity`, value: { description: 'Relative humidity.', units: 'ratio' } }); metas.push({ path: `${pathRoot}.outside.absoluteHumidity`, value: { description: 'Absolute humidity.', units: 'ratio' } }); metas.push({ path: `${pathRoot}.wind.averageSpeed`, value: { description: 'Average wind speed.', units: 'm/s' } }); metas.push({ path: `${pathRoot}.wind.speedTrue`, value: { description: 'True wind speed.', units: 'm/s' } }); metas.push({ path: `${pathRoot}.wind.directionTrue`, value: { description: 'The wind direction relative to true north.', units: 'rad' } }); metas.push({ path: `${pathRoot}.wind.gust`, value: { description: 'Maximum wind gust.', units: 'm/s' } }); metas.push({ path: `${pathRoot}.wind.gustDirectionTrue`, value: { description: 'Maximum wind gust direction.', units: 'rad' } }); metas.push({ path: `${pathRoot}.wind.gust`, value: { description: 'Maximum wind gust.', units: 'm/s' } }); metas.push({ path: `${pathRoot}.water.level`, value: { description: 'Water level.', units: 'm' } }); metas.push({ path: `${pathRoot}.water.levelTendency`, value: { description: 'Integer value indicating water level tendency e.g. 0 = steady, etc.' } }); metas.push({ path: `${pathRoot}.water.levelTendencyType`, value: { description: 'Description for the value of levelTendency e.g. steady, increasing, decreasing.' } }); metas.push({ path: `${pathRoot}.water.waves.significantHeight`, value: { description: 'Significant wave height.', units: 'm' } }); metas.push({ path: `${pathRoot}.water.waves.period`, value: { description: 'Wave period.', units: 'ms' } }); metas.push({ path: `${pathRoot}.water.waves.direction`, value: { description: 'Wave direction.', units: 'rad' } }); metas.push({ path: `${pathRoot}.water.swell.significantHeight`, value: { description: 'Significant swell height.', units: 'm' } }); metas.push({ path: `${pathRoot}.water.swell.period`, value: { description: 'Swell period.', units: 'ms' } }); metas.push({ path: `${pathRoot}.water.swell.directionTrue`, value: { description: 'Swell direction.', units: 'rad' } }); return metas; };