UNPKG

@yachteye/signalk-weather-plugin

Version:
451 lines (450 loc) 22.5 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.roundToDecimalPlaces = void 0; const openweather_1 = __importDefault(require("./openweather")); const updater_1 = __importDefault(require("./updater")); const controller_1 = __importDefault(require("./controller")); const openapi = require('../public/openApi.json'); /** * YachtEye OpenWeather SignalK Plugin. * @param {*} app The SignalK app. * @returns The plugin object. */ module.exports = (app) => { const plugin = { id: 'signalk-weather-plugin', name: 'YachtEye open-weather plugin', checkIntervalMinutes: 15, restartPlugin: null, intervalTimer: null, unsubscribes: [], lastWeatherTimestamp: 0, lastPosition: null, controller: null, /** * Start the plugin. * @param settings the configuration data entered via the Plugin Config screen. * @param restartPlugin a function that can be called by the plugin to restart itself. */ start: (settings, restartPlugin) => { app.debug(`starting...`); plugin.restartPlugin = restartPlugin; try { if (plugin.intervalTimer !== null) { clearInterval(plugin.intervalTimer); plugin.intervalTimer = null; } const openWeatherApi = new openweather_1.default(settings.licenseCode); const updater = new updater_1.default(app, plugin.id); plugin.controller = new controller_1.default(app, openWeatherApi, updater); plugin.intervalTimer = setInterval(plugin.check, plugin.checkIntervalMinutes * 60 * 1000, openWeatherApi, updater, settings); setTimeout(() => { plugin.check(openWeatherApi, updater, settings); }, 100 * 1000); plugin.registerWithRouter = (router) => { app.debug('start(): register currentWeatherAt and forecastWeatherAt'); if (plugin.controller !== null) { router.get('/currentWeatherAt', plugin.controller.currentWeatherAt); router.get('/forecastWeatherAt', plugin.controller.forecastWeatherAt); } else { throw new Error('WeatherController missing'); } }; plugin.getOpenApi = () => { return openapi; }; app.handleMessage(plugin.id, plugin.meta()); app.setPluginStatus(`Started, interval=${settings.interval} (minutes).`); } catch (err) { app.setPluginError(`Error in start(): ${err?.message}`); } }, /** * Stop the plugin. */ stop: () => { if (plugin.intervalTimer !== null) { clearInterval(plugin.intervalTimer); plugin.intervalTimer = null; } plugin.unsubscribes.forEach(f => f()); plugin.unsubscribes = []; plugin.lastWeatherTimestamp = 0; plugin.lastPosition = null; app.debug(`Stopped.`); app.setPluginStatus(`Stopped.`); }, check: async (ow, updater, settings) => { app.debug(`check()`); try { const yachtPosition = app.getSelfPath('navigation.position'); const latLon = plugin.getPosition(yachtPosition); const now = Date.now(); if (latLon !== null) { const travelled = plugin.lastPosition !== null && distanceNM(plugin.lastPosition, latLon) > settings.travelledDistanceThresholdNm; if (travelled || (now - plugin.lastWeatherTimestamp > settings.interval * 60 * 1000)) { app.debug(`check(): updating local weather, exceeded distance-travelled=${travelled}.`); plugin.updateLocalWeather(ow, updater, latLon, settings); plugin.lastWeatherTimestamp = now; plugin.lastPosition = latLon; } } else { app.setPluginError(`check(): no yacht position`); } } catch (err) { app.setPluginError(`Error in check(): ${err?.message}`); } }, updateLocalWeather: async (ow, updater, yachtPosition, settings) => { app.debug(`updateLocalWeather() for (${yachtPosition.latitude.toFixed(5)}, ${yachtPosition.longitude.toFixed(5)})`); try { const currentWeather = await ow.currentWeatherForPosition(yachtPosition.latitude, yachtPosition.longitude); updater.handleCurrentWeatherResult(currentWeather); const dailyForecast = await ow.dailyForecastForPosition(yachtPosition.latitude, yachtPosition.longitude, 6); updater.handleDailyForecastResult(dailyForecast); // 3 hour forecast for the coming 48 hours. const threeHourForecasts = await ow.threeHourlyForecastForPosition(yachtPosition.latitude, yachtPosition.longitude, 16); updater.handleThreeHourForecastResult(threeHourForecasts, 'resources.weather.actual.forecast', false); } catch (err) { app.setPluginError(`Error in updateLocalWeather(): ${err?.message}`); } }, getPosition(skData) { if (skData !== undefined && skData.value !== null && typeof skData.value === 'object' && 'latitude' in skData.value && 'longitude' in skData.value && typeof skData.value.latitude === 'number' && typeof skData.value.longitude === 'number') { return { latitude: skData.value.latitude, longitude: skData.value.longitude }; } return null; }, meta: () => { const metaDelta = { context: "vessels.self", updates: [ { timestamp: new Date().toISOString(), meta: [ // Current weather. { path: 'resources.weather.*.current.airpressure', value: { units: "Pa", displayName: "Atmospheric pressure (on sea level) at the current location", } }, { path: 'resources.weather.*.current.temperature', value: { units: "K", displayName: "Temperature at the current location", }, }, { path: 'resources.weather.*.current.perceivedTemperature', value: { units: "K", displayName: "Perceived ('feels like') temperature at the current location", }, }, { path: 'resources.weather.*.current.cloudiness', value: { units: "ratio", displayName: "Cloudiness", }, }, { path: 'resources.weather.*.current.humidity', value: { units: "ratio", displayName: "Humidity", }, }, { path: 'resources.weather.*.current.winddirection', value: { units: "rad", displayName: "Wind direction", }, }, { path: 'resources.weather.*.current.windspeed', value: { units: "m/s", displayName: "Wind speed", }, }, // { // path: 'resources.weather.*.current.precipitationLastHour', // value: { // units: "mm", // displayName: "Combined Rain/Snow volume for the last hour", // }, // }, // { // path: 'resources.weather.*.current.precipitationLast3Hours', // value: { // units: "mm", // displayName: "Combined Rain/Snow volume for the last 3 hours", // }, // }, // Forecast weather. { path: 'resources.weather.actual.forecast.*.conditionId', value: { displayName: "OpenWeather condition ID (2xx: thunderstorm, 3xx: drizzle, 5xx: rain, 6xx: snow, 7xx: atmosphere, 800: clear, 8xx: cloud).", }, }, { path: 'resources.weather.actual.forecast.*.temperature', value: { units: "K", displayName: "Temperature forecast for the current location at the indicated date/time (default: 12:00 local time, morning: 06:00 local time, afternoon: 12:00 local time, evening: 18:00 local time)", }, }, // { // path: 'resources.weather.actual.forecast.morning.temperature', // value: { // units: "K", // displayName: "Temperature forecast for the current location (06:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.*.morning.temperature', // value: { // units: "K", // displayName: "Temperature forecast for the current location (06:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.afternoon.temperature', // value: { // units: "K", // displayName: "Temperature forecast for the current location (12:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.*.afternoon.temperature', // value: { // units: "K", // displayName: "Temperature forecast for the current location (12:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.evening.temperature', // value: { // units: "K", // displayName: "Temperature forecast for the current location (18:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.*.evening.temperature', // value: { // units: "K", // displayName: "Temperature forecast for the current location (18:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.*.temperatureNight', // value: { // units: "K", // displayName: "Temperature forecast for the current location (00:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.*.temperatureNoon', // value: { // units: "K", // displayName: "Temperature forecast for the current location (12:00 local time)", // }, // }, // { // path: 'resources.weather.actual.forecast.*.temperatureEvening', // value: { // units: "K", // displayName: "Temperature forecast for the current location (18:00 local time)", // }, // }, { path: 'resources.weather.actual.forecast.*.temperatureMin', value: { units: "K", displayName: "Minimum daily Temperature forecast for the current location", }, }, { path: 'resources.weather.actual.forecast.*.temperatureMax', value: { units: "K", displayName: "Maximum daily Temperature forecast for the current location", }, }, { path: 'resources.weather.actual.forecast.*.winddirection', value: { units: "rad", displayName: "Wind direction", }, }, { path: 'resources.weather.actual.forecast.*.windspeed', value: { units: "m/s", displayName: "Wind speed", }, }, // { // path: 'resources.weather.actual.forecast.*.precipitationLastHour', // value: { // units: "mm", // displayName: "Combined Rain/Snow volume for the last hour", // }, // }, // { // path: 'resources.weather.actual.forecast.*.precipitationLast3Hours', // value: { // units: "mm", // displayName: "Combined Rain/Snow volume for the last 3 hours", // }, // }, { path: 'resources.weather.[0-9]+.forecast.*.temperature', value: { units: "K", displayName: "Temperature forecast for the geoname location at 12:00 local time", }, }, { path: 'resources.weather.[0-9]+.forecast.*.temperatureMin', value: { units: "K", displayName: "Minimum daily Temperature forecast for the geoname location", }, }, { path: 'resources.weather.[0-9]+.forecast.*.temperatureMax', value: { units: "K", displayName: "Maximum daily Temperature forecast for the geoname location", }, }, { path: 'resources.weather.[0-9]+.forecast.*.conditionId', value: { displayName: "OpenWeather condition ID (2xx: thunderstorm, 3xx: drizzle, 5xx: rain, 6xx: snow, 7xx: atmosphere, 800: clear, 8xx: cloud).", }, }, { path: 'resources.weather.[0-9]+.forecast.*.winddirection', value: { units: "rad", displayName: "Wind direction", }, }, { path: 'resources.weather.[0-9]+.forecast.*.windspeed', value: { units: "m/s", displayName: "Wind speed", }, }, ] } ] }; return metaDelta; }, schema: () => { return { type: 'object', required: ['licenseCode'], properties: { interval: { type: 'number', title: 'Interval to update the weather data (minutes)', default: 120 }, licenseCode: { type: 'string', title: 'OpenWeather license code for the API.', }, travelledDistanceThresholdNm: { type: 'number', title: 'Threshold for travelled distance (NM), update local weather if exceeded.', default: 20 }, } }; }, }; return plugin; }; // Utilities: const NauticalMileInMeter = 1852.0; const EarthRadiusKM = 6371.01; const EarthRadiusNM = 3441.596; const degreeToRadian = (degree) => { return (degree * Math.PI) / 180.0; }; const radianToDegree = (radian) => { return (radian * 180.0) / Math.PI; }; const distanceNM = (p1, p2) => { return distanceBetweenNauticalMiles(p1.latitude, p1.longitude, p2.latitude, p2.longitude); }; /** * Calculate the distance from point-1 (from) to point-2 (to). * Using Spherical Law of Cosines. * @param fromLatitude Latitude of point 1 (degrees). * @param fromLongitude Longitude of point 1 (degrees). * @param toLatitude Latitude of point 2 (degrees). * @param toLongitude Longitude of point 2 (degrees). * @returns the distance in Nautical miles. */ const distanceBetweenNauticalMiles = (fromLatitude, fromLongitude, toLatitude, toLongitude) => { let rv; const fromLatitudeRadians = degreeToRadian(fromLatitude); const toLatitudeRadians = degreeToRadian(toLatitude); rv = EarthRadiusNM * Math.acos(Math.sin(fromLatitudeRadians) * Math.sin(toLatitudeRadians) + Math.cos(fromLatitudeRadians) * Math.cos(toLatitudeRadians) * Math.cos(degreeToRadian(toLongitude) - degreeToRadian(fromLongitude))); if (isNaN(rv)) { rv = 0; } return rv; }; const distanceBetweenMeters = (fromLatitude, fromLongitude, toLatitude, toLongitude) => { const miles = distanceBetweenNauticalMiles(fromLatitude, fromLongitude, toLatitude, toLongitude); return miles * NauticalMileInMeter; }; const bearingTo = (fromLatitude, fromLongitude, toLatitude, toLongitude) => { const lat1 = degreeToRadian(fromLatitude); const lat2 = degreeToRadian(toLatitude); const dlon = degreeToRadian(toLongitude - fromLongitude); const y = Math.sin(dlon) * Math.cos(lat2); const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(dlon); const brng = (radianToDegree(Math.atan2(y, x)) + 360) % 360; return brng; }; /** * Round a number to the specified decimal places. * @param input Number to round. * @param decimalPlaces Number of decimal places to use (positive integer). * @returns The rounded input. */ const roundToDecimalPlaces = (input, decimalPlaces) => { if (decimalPlaces > 0 && Number.isInteger(decimalPlaces)) { const multiplier = Math.pow(10, decimalPlaces); return Math.round((input + Number.EPSILON) * multiplier) / multiplier; } else { return Math.round(input); } }; exports.roundToDecimalPlaces = roundToDecimalPlaces;