signalk-server
Version:
An implementation of a [Signal K](http://signalk.org) server for boats.
465 lines • 17 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WeatherApi = void 0;
/* eslint-disable @typescript-eslint/no-explicit-any */
const debug_1 = require("../../debug");
const debug = (0, debug_1.createDebug)('signalk-server:api:weather');
const __1 = require("../");
const server_api_1 = require("@signalk/server-api");
const WEATHER_API_PATH = `/signalk/v2/api/weather`;
class WeatherApi {
app;
weatherProviders = new Map();
defaultProviderId;
constructor(app) {
this.app = app;
}
async start() {
this.initApiEndpoints();
return Promise.resolve();
}
// ***** Plugin Interface methods *****
// Register plugin as provider.
register(pluginId, provider) {
debug(`** Registering provider(s)....${pluginId} ${provider}`);
if (!pluginId || !provider) {
throw new Error(`Error registering provider ${pluginId}!`);
}
if (!(0, server_api_1.isWeatherProvider)(provider)) {
throw new Error(`${pluginId} is missing WeatherProvider properties/methods!`);
}
else {
if (!this.weatherProviders.has(pluginId)) {
this.weatherProviders.set(pluginId, provider);
}
if (this.weatherProviders.size === 1) {
this.defaultProviderId = pluginId;
}
}
debug(`No. of WeatherProviders registered =`, this.weatherProviders.size);
}
// Unregister plugin as provider.
unRegister(pluginId) {
if (!pluginId) {
return;
}
debug(`** Request to un-register plugin.....${pluginId}`);
if (!this.weatherProviders.has(pluginId)) {
debug(`** NOT FOUND....${pluginId}... cannot un-register!`);
return;
}
debug(`** Un-registering weather provider....${pluginId}`);
this.weatherProviders.delete(pluginId);
if (pluginId === this.defaultProviderId) {
this.defaultProviderId = undefined;
}
// update defaultProviderId if required
if (this.weatherProviders.size !== 0 && !this.defaultProviderId) {
this.defaultProviderId = this.weatherProviders.keys().next().value;
}
debug(`Remaining number of Weather Providers registered =`, this.weatherProviders.size, 'defaultProvider =', this.defaultProviderId);
}
/** Server API methods */
/**
* @description Return array of observations for the provided location
* @param position Location for which to retrieve observation data
* @param options Parameters to filter the returned data
*/
async getObservations(position, options) {
const q = this.parseQueryOptions({
position: position,
options: options
});
if (this.defaultProviderId &&
this.weatherProviders.has(this.defaultProviderId)) {
debug(`Using default provider...${this.defaultProviderId}`);
return await this.weatherProviders
.get(this.defaultProviderId)
?.methods.getObservations(q.position, q.options);
}
else {
throw new Error(`Provider not found!`);
}
}
/**
* @description Return array of forecasts for the provided location
* @param position Location for which to retrieve forecast data
* @param type Type of forecast point | daily
* @param options Parameters to filter the returned data
*/
async getForecasts(position, type, options) {
const q = this.parseQueryOptions({
position: position,
options: options
});
if (this.defaultProviderId &&
this.weatherProviders.has(this.defaultProviderId)) {
debug(`Using default provider...${this.defaultProviderId}`);
const fdata = await this.weatherProviders
.get(this.defaultProviderId)
?.methods.getForecasts(q.position, type, q.options);
return fdata
? fdata.filter((i) => {
return i.type === type;
})
: [];
}
else {
throw new Error(`Provider not found!`);
}
}
/**
* @description Return array of warnings for the provided location
* @param position Location for which to retrieve warning data
*/
async getWarnings(position) {
const q = this.parseQueryOptions({
position: position
});
if (this.defaultProviderId &&
this.weatherProviders.has(this.defaultProviderId)) {
debug(`Using default provider...${this.defaultProviderId}`);
return await this.weatherProviders
.get(this.defaultProviderId)
?.methods.getWarnings(q.position);
}
else {
throw new Error(`Provider not found!`);
}
}
// *************************************
updateAllowed(request) {
return this.app.securityStrategy.shouldAllowPut(request, 'vessels.self', null, 'weather');
}
/** @returns 1= OK, 0= invalid location, -1= location not provided */
checkLocation(req) {
if (req.query.lat && req.query.lon) {
return isNaN(Number(req.query.lat)) || isNaN(Number(req.query.lon))
? 0
: 1;
}
else {
return -1;
}
}
parseRequest(req, res, next) {
try {
debug(`Weather`, req.method, req.path, req.query, req.body);
if (['PUT', 'POST'].includes(req.method)) {
if (!this.updateAllowed(req)) {
res.status(403).json(__1.Responses.unauthorised);
}
else {
next();
}
}
else {
if (req.path === `/` || req.path === `/forecasts`) {
next();
return;
}
const l = this.checkLocation(req);
if (l === 1) {
next();
}
else if (l === 0) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: 'Invalid position data!'
});
}
else {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: 'Location not supplied!'
});
}
}
}
catch (err) {
res.status(500).json({
state: 'FAILED',
statusCode: 500,
message: err.message
});
}
}
initApiEndpoints() {
debug(`** Initialise ${WEATHER_API_PATH} endpoints. **`);
this.app.use(`${WEATHER_API_PATH}`, (req, res, next) => {
debug(`Using... ${WEATHER_API_PATH}`);
if (req.path.includes('providers')) {
next();
}
else {
return this.parseRequest(req, res, next);
}
});
this.app.get(`${WEATHER_API_PATH}`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
try {
res.status(200).json({
forecasts: {
description: 'Forecast data for the supplied location.'
},
observations: {
description: 'Observation data for the supplied location.'
},
warnings: {
description: 'Weather warnings for the supplied location.'
}
});
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
// return list of weather providers
this.app.get(`${WEATHER_API_PATH}/_providers`, async (req, res) => {
debug(`**route = ${req.method} ${req.path}`);
try {
const r = {};
this.weatherProviders.forEach((v, k) => {
r[k] = {
name: v.name,
isDefault: k === this.defaultProviderId
};
});
res.status(200).json(r);
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
// return default weather provider identifier
this.app.get(`${WEATHER_API_PATH}/_providers/_default`, async (req, res) => {
debug(`**route = ${req.method} ${req.path}`);
try {
res.status(200).json({
id: this.defaultProviderId
});
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
// change weather provider
this.app.post(`${WEATHER_API_PATH}/_providers/_default/:id`, async (req, res) => {
debug(`**route = ${req.method} ${req.path}`);
try {
if (!req.params.id) {
throw new Error('Provider id not supplied!');
}
if (this.weatherProviders.has(req.params.id)) {
this.defaultProviderId = req.params.id;
res.status(200).json({
statusCode: 200,
state: 'COMPLETED',
message: `Default provider set to ${req.params.id}.`
});
}
else {
throw new Error(`Provider ${req.params.id} not found!`);
}
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
// return observation data at the provided lat / lon
this.app.get(`${WEATHER_API_PATH}/observations`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
try {
const q = this.parseQueryOptions(req.query);
const r = await this.useProvider(req).getObservations(q.position, q.options);
res.status(200).json(r);
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
this.app.get(`${WEATHER_API_PATH}/forecasts`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
try {
res.status(200).json({
daily: {
description: 'Daily forecast data for the requested number of days.'
},
point: {
description: 'Point forecast data for the requested number of intervals.'
}
});
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
/**
* Return daily forecast data at the provided lat / lon for the supplied number of days
* ?days=x
*
*/
this.app.get(`${WEATHER_API_PATH}/forecasts/daily`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
try {
const q = this.parseQueryOptions(req.query);
const r = await this.useProvider(req).getForecasts(q.position, 'daily', q.options);
const df = r.filter((i) => {
return i.type === 'daily';
});
res.status(200).json(df);
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
// return point forecast data at the provided lat / lon
this.app.get(`${WEATHER_API_PATH}/forecasts/point`, async (req, res) => {
debug(`** ${req.method} ${req.path}`);
try {
const q = this.parseQueryOptions(req.query);
const r = await this.useProvider(req).getForecasts(q.position, 'point', q.options);
const pf = r.filter((i) => {
return i.type === 'point';
});
res.status(200).json(pf);
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
// return warning data at the provided lat / lon
this.app.get(`${WEATHER_API_PATH}/warnings`, async (req, res) => {
debug(`** route = ${req.method} ${req.path}`);
try {
const q = this.parseQueryOptions(req.query);
const r = await this.useProvider(req).getWarnings(q.position);
res.status(200).json(r);
}
catch (err) {
res.status(400).json({
statusCode: 400,
state: 'FAILED',
message: err.message
});
}
});
// error response
this.app.use(`${WEATHER_API_PATH}/*`, (err, req, res, next) => {
debug(`** route = error path **`);
const msg = {
state: err.state ?? 'FAILED',
statusCode: err.statusCode ?? 500,
message: err.message ?? 'Weather provider error!'
};
if (res.headersSent) {
console.log('EXCEPTION: headersSent');
return next(msg);
}
res.status(500).json(msg);
});
}
/** Returns provider to use as data source.
* @param req API request.
*/
useProvider(req) {
debug('** useProvider()');
if (this.weatherProviders.size === 0) {
throw new Error('No providers registered!');
}
if (req.query.provider) {
debug(`Use requested provider... ${req.query.provider}`);
if (this.weatherProviders.has(req.query.provider)) {
debug(`Requested provider found...using ${req.query.provider}`);
return this.weatherProviders.get(req.query.provider)
?.methods;
}
else {
throw new Error(`Requested provider not found! (${req.query.provider})`);
}
}
else {
if (this.defaultProviderId &&
this.weatherProviders.has(this.defaultProviderId)) {
debug(`Using default provider...${this.defaultProviderId}`);
return this.weatherProviders.get(this.defaultProviderId)
?.methods;
}
else {
throw new Error(`Default provider not found!`);
}
}
}
/**
* Parse request.query into weather provider options
* @param query req.query
*/
parseQueryOptions(query) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { lat, lon, provider, count, date, ...custom } = query;
const q = {
position: {
latitude: this.parseValueAsNumber(lat) ?? 0,
longitude: this.parseValueAsNumber(lon) ?? 0
},
options: { custom }
};
if (count) {
const n = this.parseValueAsNumber(count);
if (typeof n === 'number') {
q.options.maxCount = n;
}
}
if (date) {
const pattern = /[0-9]{4}-[0-9]{2}-[0-9]{2}/;
if (date.match(pattern)) {
q.options.startDate = date?.toString();
}
}
return q;
}
/**
* Ensure the query parameter value is a number
* @param q Query param value
*/
parseValueAsNumber(value) {
const n = Number(value);
return isNaN(n) ? undefined : n;
}
}
exports.WeatherApi = WeatherApi;
//# sourceMappingURL=index.js.map