@yachteye/signalk-weather-plugin
Version:
YachtEye open-weather plugin
451 lines (450 loc) • 22.5 kB
JavaScript
;
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;