diff --git a/.gitignore b/.gitignore index f00599659..b5e93248c 100644 --- a/.gitignore +++ b/.gitignore @@ -29,4 +29,4 @@ test/server-test-config/ssl-cert.pem test/server-test-config/ssl-key.pem test/server-test-config/plugin-config-data/ -docs/dist +docs/built diff --git a/docs/develop/plugins/README.md b/docs/develop/plugins/README.md index 63a994052..463539686 100644 --- a/docs/develop/plugins/README.md +++ b/docs/develop/plugins/README.md @@ -4,9 +4,10 @@ children: - ../webapps.md - deltas.md - configuration.md - - resource_provider_plugins.md - - ../rest-api/course_calculations.md - autopilot_provider_plugins.md + - ../rest-api/course_calculations.md + - resource_provider_plugins.md + - weather_provider_plugins.md - publishing.md --- diff --git a/docs/develop/plugins/resource_provider_plugins.md b/docs/develop/plugins/resource_provider_plugins.md index c33994f92..362236ab7 100644 --- a/docs/develop/plugins/resource_provider_plugins.md +++ b/docs/develop/plugins/resource_provider_plugins.md @@ -61,7 +61,7 @@ module.exports = function (app) { const plugin = { id: 'mypluginid', - name: 'My Resource Providerplugin' + name: 'My Resource Provider plugin' } const routesProvider: ResourceProvider = { diff --git a/docs/develop/plugins/weather_provider_plugins.md b/docs/develop/plugins/weather_provider_plugins.md new file mode 100644 index 000000000..9bf1cd05b --- /dev/null +++ b/docs/develop/plugins/weather_provider_plugins.md @@ -0,0 +1,88 @@ +--- +title: Weather Providers +--- + +# Weather Providers + +The Signal K server [Weather API](../rest-api/weather_api.md) provides a common set of operations for retrieving meteorological data via a "provider plugin" to facilitate communication with a weather service provider. + +A weather provider plugin is a Signal K server plugin that brokers communication with a weather provider. + +--- + +## Weather Provider Interface + +For a plugin to be a weather provider it must implement the {@link @signalk/server-api!WeatherProvider | `WeatherProvider`} Interface which provides the Signal K server with methods to pass the details contained in API requests. + +**A weather provider MUST return data as defined by the OpenAPI definition.** + +> [!NOTE] +> Multiple weather providers can be registered with the Signal K server to enable meteorogical data retrieval from multiple sources. + +## Weather Provider Interface Methods + +Weather API requests made to the Signal K server will result in the plugin's {@link @signalk/server-api!WeatherProviderMethods | `WeatherProviderMethods`} being called. + +A weather provider plugin MUST implement ALL of the {@link @signalk/server-api!WeatherProviderMethods | `WeatherProviderMethods`}: + +- {@link @signalk/server-api!WeatherProviderMethods.getObservations | `getObservations(position, options)`} + +- {@link @signalk/server-api!WeatherProviderMethods.getForecasts | `getForecasts(position, type, options)`} + +- {@link @signalk/server-api!WeatherProviderMethods.getWarnings | `getWarnings(position)`} + +> [!NOTE] +> The Weather Provider is responsible for implementing the methods and returning data in the required format! + +--- + +## Registering a Weather Provider + +Now that the plugin has implemented the required interface and methods, it can be registered as a weather provider with the SignalK server. + +The plugin registers itself as a weather provider by calling the server's {@link @signalk/server-api!WeatherProviderRegistry.registerWeatherProvider | `registerWeatherProvider`} function during startup. + +Do this within the plugin `start()` method. + +_Example._ + +```javascript +import { WeatherProvider } from '@signalk/server-api' + +module.exports = function (app) { + + const weatherProvider: WeatherProvider = { + name: 'MyWeatherService', + methods: { + getObservations: ( + position: Position, + options?: WeatherReqParams + ) => { + // fetch observation data from weather service + return observations + }, + getForecasts: ( + position: Position, + type: WeatherForecastType, + options?: WeatherReqParams + ) => { + // fetch forecasts data from weather service + return forecasts + }, + getWarnings: () => { + // Service does not provide weather warnings. + throw new Error('Not supported!') + } + } + } + + const plugin = { + id: 'mypluginid', + name: 'My Weather Provider plugin' + start: (settings: any) => { + app.registerWeatherProvider(weatherProvider) + } + } + + return plugin +``` diff --git a/docs/develop/rest-api/README.md b/docs/develop/rest-api/README.md index e2e9e7247..4b92bb3fd 100644 --- a/docs/develop/rest-api/README.md +++ b/docs/develop/rest-api/README.md @@ -9,6 +9,10 @@ children: - notifications_api.md - anchor_api.md - plugin_api.md + - resources_api.md + - weather_api.md + - anchor_api.md + - notifications_api.md --- # REST APIs @@ -26,6 +30,8 @@ APIs are available via `/signalk/v2/api/` | [`Autopilot`](./autopilot_api.md) | Provide the ability to send common commands to an autopilot via a provider plugin. | `vessels/self/autopilot` | | [Course](./course_api.md) | Set a course, follow a route, advance to next point, etc. | `vessels/self/navigation/course` | | [Resources](./resources_api.md) | Create, view, update and delete waypoints, routes, etc. | `resources` | +| [`Autopilot`](./autopilot_api.md) | Provide the ability to send common commands to an autopilot via a provider plugin. | `vessels/self/autopilot` | +| [`Weather`](./weather_api.md) | Provide the ability to surface meteorological data from weather providers. | `weather` | --- @@ -33,7 +39,7 @@ APIs are available via `/signalk/v2/api/` | Proposed API | Description | Endpoint | | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------- | -| _[`Notifications`](notifications_api.md)_ | Provide the ability to raise, update and clear notifications from multiple sources. _[View PR](https://github.com/SignalK/signalk-server/pull/1560)_ | `notifications` | | _[`Anchor`](./anchor_api.md)_ | Provide endpoints to perform operations and facilitate an anchor alarm. | `vessels/self/navigation/anchor` | +| _[`Notifications`](notifications_api.md)_ | Provide the ability to raise, update and clear notifications from multiple sources. _[View PR](https://github.com/SignalK/signalk-server/pull/1560)_ | `notifications` | --- diff --git a/docs/develop/rest-api/weather_api.md b/docs/develop/rest-api/weather_api.md new file mode 100644 index 000000000..d01de8c36 --- /dev/null +++ b/docs/develop/rest-api/weather_api.md @@ -0,0 +1,121 @@ +--- +title: Weather API +--- + +# Weather API + +The Signal K server Weather API provides a common set of operations for viewing information from weather data sources via a "provider plugin". The provider plugin facilitates the interaction with the weather service and transforms the data into the Signal K data schema. + +Requests to the Weather API are made to HTTP REST endpoints rooted at `/signalk/v2/api/weather`. + +Weather API requests require that a postion be supplied which determines the location from which the weather data is sourced. + +The following weather data sets are supported: + +- Observations +- Forecasts +- Warnings + +Following are examples of the types of requests that can be made. + +> [!NOTE] +> The data available is dependent on the weather service API and provider-plugin. + +_Example 1: Return the latest observation data for the provided location_ + +```javascript +GET "/signalk/v2/api/weather/observations?lat=5.432&lon=7.334" +``` + +_Example 2: Return the last 5 observations for the provided location_ + +```javascript +GET "/signalk/v2/api/weather/observations?lat=5.432&lon=7.334&count=5" +``` + +_Example 3: Return the daily forecast for the next seven days for the provided location_ + +```javascript +GET "/signalk/v2/api/weather/forecasts/daily?lat=5.432&lon=7.334&count=7" +``` + +_Example 4: Return point forecasts for the next 12 periods (service provider dependant) for the provided location_ + +```javascript +GET "/signalk/v2/api/weather/forecasts/point?lat=5.432&lon=7.334&count=12" +``` + +_Example 5: Return current warnings for the provided location_ + +```javascript +GET "/signalk/v2/api/weather/warnings?lat=5.432&lon=7.334" +``` + +## Providers + +The Weather API supports the registration of multiple weather provider plugins. + +The first plugin registered is set as the _default_ provider and all requests will be directed to it. + +Requests can be directed to a specific provider by using the `provider` parameter in the request with the _id_ of the provider plugin. + +_Example:_ + +```javascript +GET "/signalk/v2/api/weather/warnings?lat=5.432&lon=7.334?provider=my-weather-plugin" +``` + +> [!NOTE] Any installed weather provider can be set as the default. _See [Setting the Default provider](#setting-a-provider-as-the-default)_ + +### Listing the available Weather Providers + +To retrieve a list of installed weather provider plugins, submit an HTTP `GET` request to `/signalk/v2/api/weather/_providers`. + +The response will be an object containing all the registered weather providers, keyed by their identifier, detailing the service `name` and whether it is assigned as the _default_. + +```typescript +HTTP GET "/signalk/v2/api/weather/_providers" +``` + +_Example: List of registered weather providers showing that `open-meteo` is assigned as the default._ + +```JSON +{ + "open-meteo": { + "provider":"OpenMeteo", + "isDefault": true + }, + "openweather": { + "provider":"OpenWeather", + "isDefault": false + } +} +``` + +### Getting the Default Provider identifier + +To get the id of the _default_ provider, submit an HTTP `GET` request to `/signalk/v2/api/weather/_providers/_default`. + +_Example:_ + +```typescript +HTTP GET "//signalk/v2/api/weather/_providers" +``` + +_Response:_ + +```JSON +{ + "id":"open-meteo" +} +``` + +### Setting a Provider as the Default + +To set / change the weather provider that requests will be directed, submit an HTTP `POST` request to `/signalk/v2/api/weather/_providers/_default/{id}` where `{id}` is the identifier of the weather provider to use as the _default_. + +_Example:_ + +```typescript +HTTP POST "/signalk/v2/api/weather/_providers/_default/openweather" +``` diff --git a/docs/src/features/weather/weather.md b/docs/src/features/weather/weather.md new file mode 100644 index 000000000..2c3150f72 --- /dev/null +++ b/docs/src/features/weather/weather.md @@ -0,0 +1,55 @@ +# Working with Weather Data + +## Introduction + +This document outlines the way in which weather data is managed in Signal K and how to reliably access and use weather data from various sources. + +The Signal K specification defines an [`environment`](https://github.com/SignalK/specification/blob/master/schemas/groups/environment.json) schema which contains attributes pertaining to weather and the environment, grouped under headings such as `outside`, `inside`, `water`, `wind`, etc. + +The `environment` schema is then able to be applied to Signal K contexts such as `vessels`, `atons`, `meteo`, etc to allow Signal K client apps to reliably consume weather data. + +Additionally, the `environment` schema is used by the `Weather API` to provide access to observation and forecast information sourced from weather service providers. + +Following are the different contexts and their use. + +## 1. On Vessel sensors + +Sensors installed on a vesssel making measurements directly outside of the vessel _(e.g. temperature, humidity, etc)_ are placed in the `vessels.self` context. + +_On vessel sensor data paths:_ + +- `vessels.self.environment.outside.*` Measurements taken outside the vessel hull +- `vessels.self.environment.inside.*` Measurements taken inside the vessel hull +- `vessels.self.environment.water.*` Measurements taken relating to the water the vessel is in. + +## 2. AIS Weather Sources + +Environment data from AIS weather stations via NMEA0183 `VDM` sentences are placed in the `meteo` context, with each station identified by a unique identifier. + +_Example - AIS sourced weather data paths:_ + +- `meteo.urn:mrn:imo:mmsi:123456789:081751.environment.outside.*` +- `meteo.urn:mrn:imo:mmsi:123456789:081751.environment.inside.*` +- `meteo.urn:mrn:imo:mmsi:123456789:081751.environment.water.*` +- `meteo.urn:mrn:imo:mmsi:123456789:081751.environment.tide.*` +- `meteo.urn:mrn:imo:mmsi:123456789:081751.environment.current.*` + +## 3. Weather Service Providers _(Weather API)_ + +Weather service providers provide a collection of observations, forecasts and weather warnings for a location that can include: + +- Current and historical data (observations) +- Daily and "point in time" forecasts + over varying time periods. + +This information is updated at regular intervals (e.g. hourly) and will relate to an area (of varying size) based on the location provided. + +The nature of this data makes it more suited to a REST API rather than a websocket stream and as such the [Signal K Weather API](../../develop/rest-api/weather_api.md) is where this information is made available. + +As each weather provider tends to have different interfaces to source information, [Signal K Server plugins](../../develop/plugins/weather_provider_plugins.md) provide the vehicle for fetching and transforming the data from the various data sources and making it available via the Weather API. + +The Weather API supports the use of multiple weather provider plugins with the ability to switch between them. + +_Example: Fetching weather data for a location._ + +- `GET "/signalk/v2/api/weather?lat=5.432&lon=7.334` diff --git a/packages/server-api/package.json b/packages/server-api/package.json index f789cde1f..e9e571f6e 100644 --- a/packages/server-api/package.json +++ b/packages/server-api/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { - "generate": "ts-auto-guard src/autopilotapi.ts 2>/dev/null", + "generate": "ts-auto-guard src/autopilotapi.ts", "build": "npm run generate && tsc -b", "watch": "tsc --watch", "prepublishOnly": "npm run build", diff --git a/packages/server-api/src/features.ts b/packages/server-api/src/features.ts index f80864c73..c6cb4594d 100644 --- a/packages/server-api/src/features.ts +++ b/packages/server-api/src/features.ts @@ -64,8 +64,9 @@ export interface FeatureInfo { } export type SignalKApiId = - | 'resources' + | 'weather' | 'course' + | 'resources' | 'history' | 'autopilot' | 'anchor' diff --git a/packages/server-api/src/index.ts b/packages/server-api/src/index.ts index 59131c4aa..f193ff064 100644 --- a/packages/server-api/src/index.ts +++ b/packages/server-api/src/index.ts @@ -10,6 +10,7 @@ export * from './autopilotapi' export * from './autopilotapi.guard' export * from './propertyvalues' export * from './brand' +export * from './weatherapi' export * from './streambundle' export * from './subscriptionmanager' export * as history from './history' diff --git a/packages/server-api/src/serverapi.ts b/packages/server-api/src/serverapi.ts index fa51afdda..18a50ef6e 100644 --- a/packages/server-api/src/serverapi.ts +++ b/packages/server-api/src/serverapi.ts @@ -4,6 +4,7 @@ import { WithFeatures, PropertyValuesEmitter, ResourceProviderRegistry, + WeatherProviderRegistry, Delta } from '.' import { CourseApi } from './course' @@ -28,6 +29,7 @@ export interface ServerAPI extends PropertyValuesEmitter, ResourceProviderRegistry, AutopilotProviderRegistry, + WeatherProviderRegistry, WithFeatures, CourseApi, SelfIdentity { diff --git a/packages/server-api/src/weatherapi.guard.ts b/packages/server-api/src/weatherapi.guard.ts new file mode 100644 index 000000000..004b44ad5 --- /dev/null +++ b/packages/server-api/src/weatherapi.guard.ts @@ -0,0 +1,23 @@ +/* + * Generated type guards for "weatherapi.ts". + * WARNING: Do not manually change this file. + */ +import { WeatherProvider } from "./weatherapi"; + +export function isWeatherProvider(obj: unknown): obj is WeatherProvider { + const typedObj = obj as WeatherProvider + return ( + (typedObj !== null && + typeof typedObj === "object" || + typeof typedObj === "function") && + typeof typedObj["name"] === "string" && + (typedObj["methods"] !== null && + typeof typedObj["methods"] === "object" || + typeof typedObj["methods"] === "function") && + (typeof typedObj["methods"]["pluginId"] === "undefined" || + typeof typedObj["methods"]["pluginId"] === "string") && + typeof typedObj["methods"]["getObservations"] === "function" && + typeof typedObj["methods"]["getForecasts"] === "function" && + typeof typedObj["methods"]["getWarnings"] === "function" + ) +} diff --git a/packages/server-api/src/weatherapi.ts b/packages/server-api/src/weatherapi.ts new file mode 100644 index 000000000..f17a27282 --- /dev/null +++ b/packages/server-api/src/weatherapi.ts @@ -0,0 +1,258 @@ +import { Position } from '.' + +export interface WeatherApi { + register: (pluginId: string, provider: WeatherProvider) => void + unRegister: (pluginId: string) => void +} + +export interface WeatherProviderRegistry { + /** + * Used by _Weather Provider plugins_ to register the weather service from which the data is sourced. + * See [`Weather Provider Plugins`](../../../docs/develop/plugins/weather_provider_plugins.md#registering-a-weather-provider) for details. + * + * @category Weather API + */ + registerWeatherProvider: (provider: WeatherProvider) => void +} + +/** + * @hidden visible through ServerAPI + */ +export interface WeatherProviders { + [id: string]: { + name: string + isDefault: boolean + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const isWeatherProvider = (obj: any) => { + const typedObj = obj + return ( + ((typedObj !== null && typeof typedObj === 'object') || + typeof typedObj === 'function') && + typeof typedObj['name'] === 'string' && + ((typedObj['methods'] !== null && + typeof typedObj['methods'] === 'object') || + typeof typedObj['methods'] === 'function') && + (typeof typedObj['methods']['pluginId'] === 'undefined' || + typeof typedObj['methods']['pluginId'] === 'string') && + typeof typedObj['methods']['getObservations'] === 'function' && + typeof typedObj['methods']['getForecasts'] === 'function' && + typeof typedObj['methods']['getWarnings'] === 'function' + ) +} + +export interface WeatherProvider { + name: string + methods: WeatherProviderMethods +} + +export interface WeatherProviderMethods { + pluginId?: string + + /** + * Retrieves observation data from the weather provider for the supplied position. + * The returned array of observations should be ordered in descending date order. + * + * @category Weather API + * + * @param position Location of interest + * @param options Options + * + * @example + ```javascript + getObservations({latitude: 16.34765, longitude: 12.5432}, {maxCount: 1}); + ``` + + ```JSON + [ + { + "date": "2024-05-03T06:00:00.259Z", + "type": "observation", + "outside": { ... } + }, + { + "date": "2024-05-03T05:00:00.259Z", + "type": "observation", + "outside": { ... } + } + ] + ``` + */ + getObservations: ( + position: Position, + options?: WeatherReqParams + ) => Promise + + /** + * Retrieves forecast data from the weather provider for the supplied position, forecast type and number of intervals. + * The returned array of forecasts should be ordered in ascending date order. + * + * @category Weather API + * + * @param position Location of interest + * @param type Type of forecast point | daily + * @param options Options + * + * @example + * Retrieve point forecast data for the next eight point intervalss + ```javascript + getForecasts( + {latitude: 16.34765, longitude: 12.5432}, + 'point', + {maxCount: 8} + ); + ``` + + ```JSON + [ + { + "date": "2024-05-03T06:00:00.259Z", + "type": "point", + "outside": { ... } + }, + { + "date": "2024-05-03T05:00:00.259Z", + "type": "point", + "outside": { ... } + } + ] + ``` + */ + getForecasts: ( + position: Position, + type: WeatherForecastType, + options?: WeatherReqParams + ) => Promise + + /** + * Retrieves warning data from the weather provider for the supplied position. + * The returned array of warnings should be ordered in ascending date order. + * + * @category Weather API + * + * @param position Location of interest + * + * @example + ```javascript + getWarnings({latitude: 16.34765, longitude: 12.5432}); + ``` + + ```JSON + [ + { + "startTime": "2024-05-03T05:00:00.259Z", + "endTime": "2024-05-03T08:00:00.702Z", + "details": "Strong wind warning.", + "source": "MyWeatherService", + "type": "Warning" + } + ] + ``` + */ + getWarnings: (position: Position) => Promise +} + +/** + * @hidden visible through ServerAPI + */ +export interface WeatherWarning { + startTime: string + endTime: string + details: string + source: string + type: string +} + +/** + * Request options + * + * @prop maxCount Maximum number of records to return + * @prop startDate Start date of forecast / observation data (format: YYYY-MM-DD) + */ +export interface WeatherReqParams { + maxCount?: number + startDate?: string +} + +/** + * @hidden visible through ServerAPI + */ +export type WeatherForecastType = 'daily' | 'point' +/** + * @hidden visible through ServerAPI + */ +export type WeatherDataType = WeatherForecastType | 'observation' + +// Aligned with Signal K environment specification +/** + * @hidden visible through ServerAPI + */ +export interface WeatherData { + description?: string + date: string + type: WeatherDataType // daily forecast, point-in-time forecast, observed values + outside?: { + minTemperature?: number + maxTemperature?: number + feelsLikeTemperature?: number + precipitationVolume?: number + absoluteHumidity?: number + horizontalVisibility?: number + uvIndex?: number + cloudCover?: number + temperature?: number + dewPointTemperature?: number + pressure?: number + pressureTendency?: TendencyKind + relativeHumidity?: number + precipitationType?: PrecipitationKind + } + water?: { + temperature?: number + level?: number + levelTendency?: TendencyKind + surfaceCurrentSpeed?: number + surfaceCurrentDirection?: number + salinity?: number + waveSignificantHeight?: number + wavePeriod?: number + waveDirection?: number + swellHeight?: number + swellPeriod?: number + swellDirection?: number + } + wind?: { + speedTrue?: number + directionTrue?: number + gust?: number + gustDirection?: number + } + sun?: { + sunrise?: string + sunset?: string + } +} + +/** + * @hidden visible through ServerAPI + */ +export type TendencyKind = + | 'steady' + | 'decreasing' + | 'increasing' + | 'not available' + +/** + * @hidden visible through ServerAPI + */ +export type PrecipitationKind = + | 'reserved' + | 'rain' + | 'thunderstorm' + | 'freezing rain' + | 'mixed/ice' + | 'snow' + | 'reserved' + | 'not available' diff --git a/src/api/index.ts b/src/api/index.ts index 206298579..349c47109 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -4,6 +4,7 @@ import { WithSecurityStrategy } from '../security' import { CourseApi, CourseApplication } from './course' import { FeaturesApi } from './discovery' import { ResourcesApi } from './resources' +import { WeatherApi } from './weather' import { AutopilotApi } from './autopilot' import { SignalKApiId, WithFeatures } from '@signalk/server-api' @@ -62,6 +63,11 @@ export const startApis = ( ;(app as any).courseApi = courseApi apiList.push('course') + const weatherApi = new WeatherApi(app) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ;(app as any).weatherApi = weatherApi + apiList.push('weather') + const autopilotApi = new AutopilotApi(app) // eslint-disable-next-line @typescript-eslint/no-explicit-any ;(app as any).autopilotApi = autopilotApi @@ -72,6 +78,7 @@ export const startApis = ( Promise.all([ resourcesApi.start(), courseApi.start(), + weatherApi.start(), featuresApi.start(), autopilotApi.start() ]) diff --git a/src/api/swagger.ts b/src/api/swagger.ts index 29e388bfb..e5fecb7bd 100644 --- a/src/api/swagger.ts +++ b/src/api/swagger.ts @@ -8,6 +8,7 @@ import { resourcesApiRecord } from './resources/openApi' import { autopilotApiRecord } from './autopilot/openApi' import { securityApiRecord } from './security/openApi' import { discoveryApiRecord } from './discovery/openApi' +import { weatherApiRecord } from './weather/openApi' import { appsApiRecord } from './apps/openApi' import { historyApiRecord } from './history/openApi' import { PluginId, PluginManager } from '../interfaces/plugins' @@ -32,6 +33,7 @@ const apiDocs = [ courseApiRecord, notificationsApiRecord, resourcesApiRecord, + weatherApiRecord, securityApiRecord, historyApiRecord ].reduce((acc, apiRecord: OpenApiRecord) => { diff --git a/src/api/weather/index.ts b/src/api/weather/index.ts new file mode 100644 index 000000000..96da75767 --- /dev/null +++ b/src/api/weather/index.ts @@ -0,0 +1,477 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { createDebug } from '../../debug' +const debug = createDebug('signalk-server:api:weather') + +import { IRouter, NextFunction, Request, Response } from 'express' +import { WithSecurityStrategy } from '../../security' + +import { Responses } from '../' +import { SignalKMessageHub } from '../../app' + +import { + WeatherProvider, + WeatherProviders, + WeatherProviderMethods, + WeatherData, + isWeatherProvider, + Position, + WeatherReqParams +} from '@signalk/server-api' + +const WEATHER_API_PATH = `/signalk/v2/api/weather` + +interface WeatherApplication + extends WithSecurityStrategy, + SignalKMessageHub, + IRouter {} + +export class WeatherApi { + private weatherProviders: Map = new Map() + + private defaultProviderId?: string + + constructor(private app: WeatherApplication) {} + + async start() { + this.initApiEndpoints() + return Promise.resolve() + } + + // ***** Plugin Interface methods ***** + + // Register plugin as provider. + register(pluginId: string, provider: WeatherProvider) { + debug(`** Registering provider(s)....${pluginId} ${provider}`) + + if (!pluginId || !provider) { + throw new Error(`Error registering provider ${pluginId}!`) + } + if (!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: string) { + 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 autopilot 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 + ) + } + + // ************************************* + + private updateAllowed(request: Request): boolean { + return this.app.securityStrategy.shouldAllowPut( + request, + 'vessels.self', + null, + 'weather' + ) + } + + /** @returns 1= OK, 0= invalid location, -1= location not provided */ + private checkLocation(req: Request): number { + if (req.query.lat && req.query.lon) { + return isNaN(Number(req.query.lat)) || isNaN(Number(req.query.lon)) + ? 0 + : 1 + } else { + return -1 + } + } + + private parseRequest(req: Request, res: Response, next: NextFunction) { + 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(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: any) { + res.status(500).json({ + state: 'FAILED', + statusCode: 500, + message: err.message + }) + } + } + + private initApiEndpoints() { + debug(`** Initialise ${WEATHER_API_PATH} endpoints. **`) + + this.app.use( + `${WEATHER_API_PATH}`, + (req: Request, res: Response, next: NextFunction) => { + 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: Request, res: Response) => { + 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: any) { + 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: Request, res: Response) => { + debug(`**route = ${req.method} ${req.path}`) + try { + const r: WeatherProviders = {} + this.weatherProviders.forEach((v: WeatherProvider, k: string) => { + r[k] = { + name: v.name, + isDefault: k === this.defaultProviderId + } + }) + res.status(200).json(r) + } catch (err: any) { + 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: Request, res: Response) => { + debug(`**route = ${req.method} ${req.path}`) + try { + res.status(200).json({ + id: this.defaultProviderId + }) + } catch (err: any) { + 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: Request, res: Response) => { + 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: any) { + 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: Request, res: Response) => { + 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: any) { + res.status(400).json({ + statusCode: 400, + state: 'FAILED', + message: err.message + }) + } + } + ) + + this.app.get( + `${WEATHER_API_PATH}/forecasts`, + async (req: Request, res: Response) => { + 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: any) { + 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: Request, res: Response) => { + 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: WeatherData) => { + return i.type === 'daily' + }) + res.status(200).json(df) + } catch (err: any) { + 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: Request, res: Response) => { + 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: WeatherData) => { + return i.type === 'point' + }) + res.status(200).json(pf) + } catch (err: any) { + 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: Request, res: Response) => { + 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: any) { + res.status(400).json({ + statusCode: 400, + state: 'FAILED', + message: err.message + }) + } + } + ) + + // error response + this.app.use( + `${WEATHER_API_PATH}/*`, + (err: any, req: Request, res: Response, next: NextFunction) => { + 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. + */ + private useProvider(req: Request): WeatherProviderMethods { + 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 as string)) { + debug(`Requested provider found...using ${req.query.provider}`) + return this.weatherProviders.get(req.query.provider as string) + ?.methods as WeatherProviderMethods + } 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 as string) + ?.methods as WeatherProviderMethods + } else { + throw new Error(`Default provider not found!`) + } + } + } + + /** + * Parse request.query into weather provider options + * @param query req.query + */ + private parseQueryOptions(query: any): WeatherReqOptions { + const q: WeatherReqOptions = { + position: { + latitude: this.parseValueAsNumber(query.lat) ?? 0, + longitude: this.parseValueAsNumber(query.lon) ?? 0 + }, + options: {} + } + if ('count' in query) { + const n = this.parseValueAsNumber(query.count) + if (typeof n === 'number') { + q.options.maxCount = n + } + } + if ('date' in query) { + const pattern = /[0-9]{4}-[0-9]{2}-[0-9]{2}/ + if ((query.date as string).match(pattern)) { + q.options.startDate = query.date?.toString() + } + } + return q + } + + /** + * Ensure the query parameter value is a number + * @param q Query param value + */ + private parseValueAsNumber(value: unknown): number | undefined { + const n = Number(value) + return isNaN(n) ? undefined : n + } +} + +interface WeatherReqOptions { + position: Position + options: WeatherReqParams +} diff --git a/src/api/weather/openApi.json b/src/api/weather/openApi.json new file mode 100644 index 000000000..a1a11b046 --- /dev/null +++ b/src/api/weather/openApi.json @@ -0,0 +1,680 @@ +{ + "openapi": "3.0.0", + "info": { + "version": "2.5.0", + "title": "Weather API", + "description": "Signal K weather API endpoints.", + "termsOfService": "http://signalk.org/terms/", + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "externalDocs": { + "url": "http://signalk.org/specification/", + "description": "Signal K specification." + }, + "servers": [ + { + "url": "/signalk/v2/api/weather" + } + ], + "tags": [ + { + "name": "Weather", + "description": "Operations to interact with weather service data." + }, + { + "name": "Provider", + "description": "Operations to view / switch providers." + } + ], + "components": { + "schemas": { + "Position": { + "type": "object", + "required": ["latitude", "longitude"], + "properties": { + "latitude": { + "type": "number", + "format": "float" + }, + "longitude": { + "type": "number", + "format": "float" + } + } + }, + "IsoTime": { + "type": "string", + "description": "Date / Time when data values were recorded", + "pattern": "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2}(?:\\.\\d*)?)((-(\\d{2}):(\\d{2})|Z)?)$", + "example": "2022-04-22T05:02:56.484Z" + }, + "WeatherDataModel": { + "type": "object", + "required": ["date", "type"], + "properties": { + "date": { + "$ref": "#/components/schemas/IsoTime" + }, + "description": { + "type": "string", + "example": "broken clouds" + }, + "type": { + "type": "string", + "enum": ["daily", "point", "observation"] + }, + "sun": { + "type": "object", + "required": ["times"], + "properties": { + "sunrise": { + "$ref": "#/components/schemas/IsoTime" + }, + "sunset": { + "$ref": "#/components/schemas/IsoTime" + } + } + }, + "outside": { + "type": "object", + "properties": { + "uvIndex": { + "type": "number", + "example": 7.5, + "description": "UV Index (1 UVI = 25mW/sqm)" + }, + "cloudCover": { + "type": "number", + "example": 85, + "description": "Amount of cloud cover (%)" + }, + "horizontalVisibility": { + "type": "number", + "example": 5000, + "description": "Visibilty (m)" + }, + "horizontalVisibilityOverRange": { + "type": "boolean", + "example": "true", + "description": "Visibilty distance is greater than the range of the measuring equipment." + }, + "temperature": { + "type": "number", + "example": 290, + "description": "Air temperature (K)" + }, + "feelsLikeTemperature": { + "type": "number", + "example": 277, + "description": "Feels-like temperature (K)" + }, + "dewPointTemperature": { + "type": "number", + "example": 260, + "description": "Dew point temperature (K)" + }, + "pressure": { + "type": "number", + "example": 10100, + "description": "Air pressure (Pa)" + }, + "pressureTendency": { + "type": "string", + "enum": ["steady", "decreasing", "increasing"], + "example": "steady", + "description": "Air pressure tendency" + }, + "absoluteHumidity": { + "type": "number", + "example": 56, + "description": "Absolute humidity (%)" + }, + "relativeHumidity": { + "type": "number", + "example": 56, + "description": "Relative humidity (%)" + }, + "precipitationType": { + "type": "string", + "enum": [ + "rain", + "thunderstorm", + "snow", + "freezing rain", + "mixed/ice" + ], + "example": "rain", + "description": "Type of preceipitation" + }, + "precipitationVolume": { + "type": "number", + "example": 56, + "description": "Amount of precipitation (mm)" + } + } + }, + "wind": { + "type": "object", + "properties": { + "averageSpeed": { + "type": "number", + "example": 9.3, + "description": "Average wind speed (m/s)" + }, + "speedTrue": { + "type": "number", + "example": 15.3, + "description": "Wind speed (m/s)" + }, + "directionTrue": { + "type": "number", + "example": 2.145, + "description": "Wind direction relative to true north (rad)" + }, + "gust": { + "type": "number", + "example": 21.6, + "description": "Wind gust (m/s)" + }, + "gustDirectionTrue": { + "type": "number", + "example": 2.6, + "description": "Wind gust direction relative to true north (rad)" + } + } + }, + "water": { + "type": "object", + "properties": { + "temperature": { + "type": "number", + "example": 21.6, + "description": "Wind gust (m/s)" + }, + "level": { + "type": "number", + "example": 11.9, + "description": "Water level (m)" + }, + "levelTendency": { + "type": "number", + "enum": ["steady", "decreasing", "increasing"], + "example": "steady", + "description": "Water level trend" + }, + "waves": { + "type": "object", + "properties": { + "significantHeight": { + "type": "number", + "example": 2.6, + "description": "Wave height (m)" + }, + "directionTrue": { + "type": "number", + "example": 2.3876, + "description": "Wave direction relative to true north (rad)" + }, + "period": { + "type": "number", + "example": 2.3876, + "description": "Wave period (m/s)" + } + } + }, + "swell": { + "type": "object", + "properties": { + "height": { + "type": "number", + "example": 2.6, + "description": "Swell height (m)" + }, + "directionTrue": { + "type": "number", + "example": 2.3876, + "description": "Swell direction relative to true north (rad)" + }, + "period": { + "type": "number", + "example": 2.3876, + "description": "Swell period (m/s)" + } + } + }, + "seaState": { + "type": "number", + "example": 2, + "description": "Sea state (Beaufort)" + }, + "salinity": { + "type": "number", + "example": 12, + "description": "Water salinity (%)" + }, + "ice": { + "type": "boolean", + "example": true, + "description": "Ice present." + } + } + }, + "current": { + "type": "object", + "properties": { + "drift": { + "type": "number", + "example": 3.4, + "description": "Surface current speed (m/s)" + }, + "set": { + "type": "number", + "example": 1.74, + "description": "Surface current direction (rad)" + } + } + } + } + }, + "WeatherWarningModel": { + "type": "object", + "required": ["startTime", "endTime"], + "properties": { + "startTime": { + "$ref": "#/components/schemas/IsoTime" + }, + "endTime": { + "$ref": "#/components/schemas/IsoTime" + }, + "source": { + "type": "string", + "description": "Name of source." + }, + "type": { + "type": "string", + "description": "Type of warning.", + "example": "Heat Advisory" + }, + "details": { + "type": "string", + "description": "Text describing the details of the warning.", + "example": "HEAT ADVISORY REMAINS IN EFFECT FROM 1 PM THIS AFTERNOON...." + } + } + } + }, + "responses": { + "200OKResponse": { + "description": "Successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Request success response", + "properties": { + "state": { + "type": "string", + "enum": ["COMPLETED"] + }, + "statusCode": { + "type": "number", + "enum": [200] + } + }, + "required": ["state", "statusCode"] + } + } + } + }, + "ErrorResponse": { + "description": "Failed operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "Request error response", + "properties": { + "state": { + "type": "string", + "enum": ["FAILED"] + }, + "statusCode": { + "type": "number", + "enum": [404] + }, + "message": { + "type": "string" + } + }, + "required": ["state", "statusCode", "message"] + } + } + } + } + }, + "parameters": { + "ProviderIdParam": { + "name": "id", + "in": "path", + "description": "Plugin id of the weather provider the request will be directed to.", + "required": true, + "schema": { + "type": "string", + "example": "myweather-provider" + } + }, + "ProviderIdQuery": { + "in": "query", + "name": "provider", + "description": "Plugin id of the weather provider the request will be directed to.", + "style": "form", + "explode": false, + "schema": { + "type": "string", + "example": "myweather-provider" + } + }, + "LatitudeParam": { + "in": "query", + "required": true, + "name": "lat", + "description": "Latitude at specified position.", + "schema": { + "type": "number", + "min": -90, + "max": 90 + } + }, + "LongitudeParam": { + "in": "query", + "required": true, + "name": "lon", + "description": "Longitude at specified position.", + "schema": { + "type": "number", + "min": -180, + "max": 180 + } + }, + "CountParam": { + "in": "query", + "required": false, + "name": "count", + "description": "Number of entries to return.", + "schema": { + "type": "number", + "min": 1 + } + }, + "StartDateParam": { + "in": "query", + "required": false, + "name": "date", + "description": "Start date for weather data to return.", + "schema": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" + } + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "JWT" + }, + "cookieAuth": { + "type": "apiKey", + "in": "cookie", + "name": "JAUTHENTICATION" + } + } + }, + "security": [{ "cookieAuth": [] }, { "bearerAuth": [] }], + "paths": { + "/observations": { + "parameters": [ + { + "$ref": "#/components/parameters/ProviderIdQuery" + }, + { + "$ref": "#/components/parameters/LatitudeParam" + }, + { + "$ref": "#/components/parameters/LongitudeParam" + }, + { + "$ref": "#/components/parameters/CountParam" + }, + { + "$ref": "#/components/parameters/StartDateParam" + } + ], + "get": { + "tags": ["Weather"], + "summary": "Retrieve observation data.", + "responses": { + "default": { + "description": "Returns the observation data for the specified location (lat / lon).", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherDataModel" + } + } + } + } + } + } + } + }, + "/forecasts/daily": { + "parameters": [ + { + "$ref": "#/components/parameters/ProviderIdQuery" + }, + { + "$ref": "#/components/parameters/LatitudeParam" + }, + { + "$ref": "#/components/parameters/LongitudeParam" + }, + { + "$ref": "#/components/parameters/CountParam" + }, + { + "$ref": "#/components/parameters/StartDateParam" + } + ], + "get": { + "tags": ["Weather"], + "summary": "Retrieve daily forecast data.", + "responses": { + "default": { + "description": "Returns daily forecast data for the specified location (lat / lon).", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherDataModel" + } + } + } + } + } + } + } + }, + "/forecasts/point": { + "parameters": [ + { + "$ref": "#/components/parameters/ProviderIdQuery" + }, + { + "$ref": "#/components/parameters/LatitudeParam" + }, + { + "$ref": "#/components/parameters/LongitudeParam" + }, + { + "$ref": "#/components/parameters/CountParam" + }, + { + "$ref": "#/components/parameters/StartDateParam" + } + ], + "get": { + "tags": ["Weather"], + "summary": "Retrieve point forecast data.", + "responses": { + "default": { + "description": "Returns point forecast data for the specified location (lat / lon).", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherDataModel" + } + } + } + } + } + } + } + }, + "/warnings": { + "parameters": [ + { + "$ref": "#/components/parameters/ProviderIdQuery" + }, + { + "$ref": "#/components/parameters/LatitudeParam" + }, + { + "$ref": "#/components/parameters/LongitudeParam" + } + ], + "get": { + "tags": ["Weather"], + "summary": "Retrieve warning data.", + "responses": { + "default": { + "description": "Returns the warning data for the specified location (lat / lon).", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/WeatherWarningModel" + } + } + } + } + } + } + } + }, + "/_providers": { + "get": { + "tags": ["Provider"], + "summary": "Retrieve list of registered providers.", + "responses": { + "default": { + "description": "Return information about the registered weather providers.", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "object", + "description": "Provider identifier", + "required": ["name", "isDefault"], + "properties": { + "name": { + "type": "string", + "description": "Provider name." + }, + "isDefault": { + "type": "boolean", + "description": "`true` if this provider is set as the default." + } + }, + "example": { + "name": "OpenMeteo", + "isDefault": true + } + } + } + } + } + } + } + } + }, + "/_providers/_default": { + "get": { + "tags": ["Provider"], + "summary": "Get the default weather provider id.", + "responses": { + "default": { + "description": "Returns the id of the provider id that is the target of requests (if provider is not specified).", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "Provider identifier." + } + }, + "example": { + "id": "open-meteo" + } + } + } + } + } + } + } + }, + "/_providers/_default/{id}": { + "parameters": [ + { + "$ref": "#/components/parameters/ProviderIdParam" + } + ], + "post": { + "tags": ["Provider"], + "summary": "Sets the default weather provider.", + "description": "Sets the proivder with the supplied `id` as the default.", + "body": { + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "Provider identifier." + } + } + }, + "responses": { + "default": { + "$ref": "#/components/responses/ErrorResponse" + }, + "200": { + "$ref": "#/components/responses/200OKResponse" + } + } + } + } + } +} diff --git a/src/api/weather/openApi.ts b/src/api/weather/openApi.ts new file mode 100644 index 000000000..cc45c1cdb --- /dev/null +++ b/src/api/weather/openApi.ts @@ -0,0 +1,8 @@ +import { OpenApiDescription } from '../swagger' +import weatherApiDoc from './openApi.json' + +export const weatherApiRecord = { + name: 'weather', + path: '/signalk/v2/api', + apiDoc: weatherApiDoc as unknown as OpenApiDescription +} diff --git a/src/interfaces/plugins.ts b/src/interfaces/plugins.ts index 9fc0b71b9..b7246e0ab 100644 --- a/src/interfaces/plugins.ts +++ b/src/interfaces/plugins.ts @@ -24,6 +24,8 @@ import { AutopilotProvider, ServerAPI, RouteDestination, + WeatherProvider, + WeatherApi, Value, SignalKApiId, SourceRef, @@ -460,6 +462,7 @@ module.exports = (theApp: any) => { onStopHandlers[plugin.id].push(() => { app.resourcesApi.unRegister(plugin.id) app.autopilotApi.unRegister(plugin.id) + app.weatherApi.unRegister(plugin.id) }) plugin.start(safeConfiguration, restart) debug('Started plugin ' + plugin.name) @@ -529,6 +532,12 @@ module.exports = (theApp: any) => { }) appCopy.putPath = putPath + const weatherApi: WeatherApi = app.weatherApi + _.omit(appCopy, 'weatherApi') // don't expose the actual weather api manager + appCopy.registerWeatherProvider = (provider: WeatherProvider) => { + weatherApi.register(plugin.id, provider) + } + const resourcesApi: ResourcesApi = app.resourcesApi _.omit(appCopy, 'resourcesApi') // don't expose the actual resource api manager appCopy.registerResourceProvider = (provider: ResourceProvider) => {