signalk-tides
Version:
Tidal predictions for the vessel's position from various online sources.
175 lines (174 loc) • 7.36 kB
JavaScript
;
/*
* Copyright 2017 Scott Bender <scott@scottbender.net> and Joachim Bakke
* Copyright 2025 Brandon Keepers <brandon@opensoul.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const noaa_js_1 = __importDefault(require("./sources/noaa.js"));
const stormglass_js_1 = __importDefault(require("./sources/stormglass.js"));
const worldtides_js_1 = __importDefault(require("./sources/worldtides.js"));
const calculations_js_1 = require("./calculations.js");
const cache_js_1 = __importDefault(require("./cache.js"));
module.exports = function (app) {
// Interval to update tide data
const defaultPeriod = 60; // 1 hour
let unsubscribes = [];
const sources = [
(0, noaa_js_1.default)(app),
(0, stormglass_js_1.default)(app),
(0, worldtides_js_1.default)(app),
];
const plugin = {
id: "tides",
name: "Tides",
// @ts-expect-error: TODO[TS]: fix Plugin type upstream
description: "Tidal predictions for the vessel's position from various online sources.",
schema: () => ({
title: "Tides API",
type: "object",
properties: {
source: {
title: "Data source",
type: "string",
"anyOf": sources.map(({ id, title }) => ({
const: id,
title
})),
default: sources[0].id,
},
// Update plugin schema with sources
...sources.reduce((properties, source) => Object.assign(properties, source.properties ?? {}), {}),
period: {
title: "Update frequency",
type: "number",
description: "How often to update tide forecast (minutes)",
default: 60,
minimum: 1,
},
}
}),
stop() {
unsubscribes.forEach((f) => f());
unsubscribes = [];
}
};
plugin.start = async function (props) {
app.debug("Starting tides-api: " + JSON.stringify(props));
let lastForecast = null;
let lastPosition = null;
const cache = new cache_js_1.default(app.getDataDirPath());
// Use the selected source, or the first one if not specified
const source = sources.find(source => source.id === props.source) || sources[0];
// Load the selected source
const provider = await source.start(props);
// Register the source as a resource provider
app.registerResourceProvider({
type: "tides",
methods: {
async listResources(query) {
if (!lastPosition)
throw new Error("No position available");
return provider({ position: lastPosition, ...query });
},
getResource() {
throw new Error("Not implemented");
},
setResource() {
throw new Error("Not implemented");
},
deleteResource() {
throw new Error("Not implemented");
}
}
});
app.subscriptionmanager.subscribe({
context: "vessels." + app.selfId,
subscribe: [
{
path: "navigation.position",
period: (props.period ?? defaultPeriod) * 60 * 1000,
policy: "fixed",
},
],
}, unsubscribes, (subscriptionError) => {
app.error("Error:" + subscriptionError);
}, updatePosition);
async function updatePosition() {
lastPosition = app.getSelfPath('navigation.position.value') || await cache.get('position') || null;
if (lastPosition) {
await cache.set('position', lastPosition);
await updateForecast();
}
}
async function updateForecast() {
if (!lastPosition) {
app.debug("No position available, cannot fetch tide data");
return;
}
try {
lastForecast = await provider({ position: lastPosition });
app.setPluginStatus("Updated tide forecast from " + source.title);
updateTides();
}
catch (e) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
app.setPluginError(e.message);
// @ts-expect-error: TODO[TS] this accepts more than just a string: https://github.com/bkeepers/signalk-server/blob/d6845ee1f915e6b729d66d2b08b15dc2e0da8e51/src/interfaces/plugins.ts#L517-L519
app.error(e);
}
}
async function updateTides(now = new Date()) {
if (!lastForecast)
return;
// Get the next two upcoming extremes
const nextTides = lastForecast.extremes.filter(({ time }) => new Date(time) >= now).slice(0, 2);
const delta = {
context: "vessels." + app.selfId,
updates: [
{
timestamp: now.toISOString(),
values: [
{
path: "environment.tide.stationName",
value: lastForecast.station.name
},
{
path: "environment.tide.heightNow",
value: (0, calculations_js_1.approximateTideHeightAt)(lastForecast.extremes, now)
},
...nextTides.flatMap(({ type, time, value }) => {
return [
{ path: `environment.tide.height${type}`, value },
{ path: `environment.tide.time${type}`, value: time },
];
})
]
},
],
};
app.debug("Sending delta: " + JSON.stringify(delta));
app.handleMessage(plugin.id, delta);
}
// Perform initial update on startup after short delay to allow gnss position to be populated
delay(4000).then(updatePosition);
// Update every minute
setInterval(updateTides, 60 * 1000);
};
function delay(time) {
return new Promise((resolve) => setTimeout(resolve, time));
}
return plugin;
};