signalk-tides 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/dependabot.yml +12 -0
- package/README.md +2 -0
- package/dist/cache.js +28 -0
- package/dist/index.js +51 -51
- package/dist/sources/noaa.js +14 -15
- package/dist/sources/stormglass.js +4 -8
- package/dist/sources/worldtides.js +5 -8
- package/package.json +5 -7
- package/public/assets/index-DDL3NhwZ.js +49 -0
- package/public/assets/index-v5sE8dbG.css +1 -0
- package/public/index.html +2 -2
- package/public/assets/index-b9CEyV9Z.css +0 -1
- package/public/assets/index-yst_zb1r.js +0 -49
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "npm"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
open-pull-requests-limit: 100
|
|
8
|
+
- package-ecosystem: "github-actions"
|
|
9
|
+
directory: "/"
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
12
|
+
open-pull-requests-limit: 100
|
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ It publishes the following [tide data](https://signalk.org/specification/1.7.0/d
|
|
|
18
18
|
* `environment.tide.timeHigh`
|
|
19
19
|
* `environment.tide.heightLow`
|
|
20
20
|
* `environment.tide.timeLow`
|
|
21
|
+
* `environment.tide.heightNow`
|
|
22
|
+
* `environment.tide.stationName`
|
|
21
23
|
|
|
22
24
|
### Tides resource
|
|
23
25
|
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const path_1 = require("path");
|
|
4
|
+
const promises_1 = require("fs/promises");
|
|
5
|
+
class FileCache {
|
|
6
|
+
path;
|
|
7
|
+
constructor(path) {
|
|
8
|
+
this.path = path;
|
|
9
|
+
}
|
|
10
|
+
async get(key) {
|
|
11
|
+
try {
|
|
12
|
+
const content = await (0, promises_1.readFile)(this.getKey(key), 'utf-8');
|
|
13
|
+
return JSON.parse(content);
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async set(key, value) {
|
|
20
|
+
// Ensure directory exists
|
|
21
|
+
await (0, promises_1.mkdir)(this.path, { recursive: true });
|
|
22
|
+
return (0, promises_1.writeFile)(this.getKey(key), JSON.stringify(value));
|
|
23
|
+
}
|
|
24
|
+
getKey(key) {
|
|
25
|
+
return (0, path_1.join)(this.path, `${key}.json`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.default = FileCache;
|
package/dist/index.js
CHANGED
|
@@ -21,11 +21,11 @@ const noaa_js_1 = __importDefault(require("./sources/noaa.js"));
|
|
|
21
21
|
const stormglass_js_1 = __importDefault(require("./sources/stormglass.js"));
|
|
22
22
|
const worldtides_js_1 = __importDefault(require("./sources/worldtides.js"));
|
|
23
23
|
const calculations_js_1 = require("./calculations.js");
|
|
24
|
+
const cache_js_1 = __importDefault(require("./cache.js"));
|
|
24
25
|
module.exports = function (app) {
|
|
25
26
|
// Interval to update tide data
|
|
26
27
|
const defaultPeriod = 60; // 1 hour
|
|
27
28
|
let unsubscribes = [];
|
|
28
|
-
let lastForecast = null;
|
|
29
29
|
const sources = [
|
|
30
30
|
(0, noaa_js_1.default)(app),
|
|
31
31
|
(0, stormglass_js_1.default)(app),
|
|
@@ -67,6 +67,9 @@ module.exports = function (app) {
|
|
|
67
67
|
};
|
|
68
68
|
plugin.start = async function (props) {
|
|
69
69
|
app.debug("Starting tides-api: " + JSON.stringify(props));
|
|
70
|
+
let lastForecast = null;
|
|
71
|
+
let lastPosition = null;
|
|
72
|
+
const cache = new cache_js_1.default(app.getDataDirPath());
|
|
70
73
|
// Use the selected source, or the first one if not specified
|
|
71
74
|
const source = sources.find(source => source.id === props.source) || sources[0];
|
|
72
75
|
// Load the selected source
|
|
@@ -76,7 +79,9 @@ module.exports = function (app) {
|
|
|
76
79
|
type: "tides",
|
|
77
80
|
methods: {
|
|
78
81
|
async listResources(query) {
|
|
79
|
-
|
|
82
|
+
if (!lastPosition)
|
|
83
|
+
throw new Error("No position available");
|
|
84
|
+
return provider({ position: lastPosition, ...query });
|
|
80
85
|
},
|
|
81
86
|
getResource() {
|
|
82
87
|
throw new Error("Not implemented");
|
|
@@ -100,45 +105,23 @@ module.exports = function (app) {
|
|
|
100
105
|
],
|
|
101
106
|
}, unsubscribes, (subscriptionError) => {
|
|
102
107
|
app.error("Error:" + subscriptionError);
|
|
103
|
-
},
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
}, updatePosition);
|
|
109
|
+
async function updatePosition() {
|
|
110
|
+
lastPosition = app.getSelfPath('navigation.position.value') || await cache.get('position') || null;
|
|
111
|
+
if (lastPosition) {
|
|
112
|
+
await cache.set('position', lastPosition);
|
|
113
|
+
await updateForecast();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function updateForecast() {
|
|
117
|
+
if (!lastPosition) {
|
|
118
|
+
app.debug("No position available, cannot fetch tide data");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
115
121
|
try {
|
|
116
|
-
lastForecast = await provider();
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// Get the first tide of this type after now
|
|
120
|
-
if (!nextTide[type] && new Date(time) > now) {
|
|
121
|
-
nextTide[type] = { time, value };
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
const delta = {
|
|
125
|
-
context: "vessels." + app.selfId,
|
|
126
|
-
updates: [
|
|
127
|
-
{
|
|
128
|
-
timestamp: now.toISOString(),
|
|
129
|
-
values: Object.entries(nextTide).flatMap(([type, { time, value }]) => {
|
|
130
|
-
return [
|
|
131
|
-
{ path: `environment.tide.height${type}`, value: value },
|
|
132
|
-
{ path: `environment.tide.time${type}`, value: time },
|
|
133
|
-
];
|
|
134
|
-
}),
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
};
|
|
138
|
-
app.debug("Sending delta: " + JSON.stringify(delta));
|
|
139
|
-
app.handleMessage(plugin.id, delta);
|
|
140
|
-
updateHeightNow();
|
|
141
|
-
app.setPluginStatus("Updated tide data");
|
|
122
|
+
lastForecast = await provider({ position: lastPosition });
|
|
123
|
+
app.setPluginStatus("Updated tide forecast from " + source.title);
|
|
124
|
+
updateTides();
|
|
142
125
|
}
|
|
143
126
|
catch (e) {
|
|
144
127
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -147,25 +130,42 @@ module.exports = function (app) {
|
|
|
147
130
|
app.error(e);
|
|
148
131
|
}
|
|
149
132
|
}
|
|
150
|
-
async function
|
|
133
|
+
async function updateTides(now = new Date()) {
|
|
151
134
|
if (!lastForecast)
|
|
152
135
|
return;
|
|
153
|
-
//
|
|
154
|
-
const
|
|
155
|
-
|
|
136
|
+
// Get the next two upcoming extremes
|
|
137
|
+
const nextTides = lastForecast.extremes.filter(({ time }) => new Date(time) >= now).slice(0, 2);
|
|
138
|
+
const delta = {
|
|
156
139
|
context: "vessels." + app.selfId,
|
|
157
140
|
updates: [
|
|
158
141
|
{
|
|
159
142
|
timestamp: now.toISOString(),
|
|
160
|
-
values: [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
143
|
+
values: [
|
|
144
|
+
{
|
|
145
|
+
path: "environment.tide.stationName",
|
|
146
|
+
value: lastForecast.station.name
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
path: "environment.tide.heightNow",
|
|
150
|
+
value: (0, calculations_js_1.approximateTideHeightAt)(lastForecast.extremes, now)
|
|
151
|
+
},
|
|
152
|
+
...nextTides.flatMap(({ type, time, value }) => {
|
|
153
|
+
return [
|
|
154
|
+
{ path: `environment.tide.height${type}`, value },
|
|
155
|
+
{ path: `environment.tide.time${type}`, value: time },
|
|
156
|
+
];
|
|
157
|
+
})
|
|
158
|
+
]
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
};
|
|
162
|
+
app.debug("Sending delta: " + JSON.stringify(delta));
|
|
163
|
+
app.handleMessage(plugin.id, delta);
|
|
164
164
|
}
|
|
165
165
|
// Perform initial update on startup after short delay to allow gnss position to be populated
|
|
166
|
-
delay(4000).then(
|
|
167
|
-
// Update
|
|
168
|
-
setInterval(
|
|
166
|
+
delay(4000).then(updatePosition);
|
|
167
|
+
// Update every minute
|
|
168
|
+
setInterval(updateTides, 60 * 1000);
|
|
169
169
|
};
|
|
170
170
|
function delay(time) {
|
|
171
171
|
return new Promise((resolve) => setTimeout(resolve, time));
|
package/dist/sources/noaa.js
CHANGED
|
@@ -5,9 +5,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.default = default_1;
|
|
7
7
|
const path_1 = __importDefault(require("path"));
|
|
8
|
-
const promises_1 =
|
|
8
|
+
const promises_1 = require("fs/promises");
|
|
9
9
|
const geolib_1 = require("geolib");
|
|
10
10
|
const moment_1 = __importDefault(require("moment"));
|
|
11
|
+
const cache_js_1 = __importDefault(require("../cache.js"));
|
|
11
12
|
const stationsUrl = `https://api.tidesandcurrents.noaa.gov/mdapi/prod/webapi/stations.json?type=tidepredictions`;
|
|
12
13
|
const dataGetterUrl = 'https://api.tidesandcurrents.noaa.gov/api/prod/datagetter';
|
|
13
14
|
const datum = 'MLLW';
|
|
@@ -16,11 +17,12 @@ function default_1(app) {
|
|
|
16
17
|
id: 'noaa',
|
|
17
18
|
title: 'NOAA (US only)',
|
|
18
19
|
async start() {
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const cache = new cache_js_1.default(path_1.default.join(app.getDataDirPath(), 'noaa'));
|
|
21
|
+
const stations = await StationList.load(cache, app);
|
|
22
|
+
// Remove old cache file
|
|
23
|
+
(0, promises_1.unlink)(path_1.default.join(app.config.configPath, 'noaastations.json')).catch(() => { });
|
|
24
|
+
return async (params) => {
|
|
25
|
+
const { position, date = (0, moment_1.default)().subtract(1, "days") } = params;
|
|
24
26
|
const station = stations.closestTo(position);
|
|
25
27
|
const endpoint = new URL(dataGetterUrl);
|
|
26
28
|
endpoint.search = new URLSearchParams({
|
|
@@ -71,21 +73,18 @@ function default_1(app) {
|
|
|
71
73
|
}
|
|
72
74
|
;
|
|
73
75
|
class StationList extends Map {
|
|
74
|
-
static async load(app) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
data = JSON.parse(await promises_1.default.readFile(filename, 'utf-8'));
|
|
79
|
-
app.debug("NOAA: Loaded cached tide stations from " + filename);
|
|
76
|
+
static async load(cache, app) {
|
|
77
|
+
let data = await cache.get('stations');
|
|
78
|
+
if (data) {
|
|
79
|
+
app.debug("NOAA: Loaded cached tide stations");
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
app.debug(`NOAA: failed to load cached tide stations: ${e}`);
|
|
81
|
+
else {
|
|
83
82
|
app.debug('NOAA: Downloading tide stations');
|
|
84
83
|
const res = await fetch(stationsUrl);
|
|
85
84
|
if (!res.ok)
|
|
86
85
|
throw new Error(`Failed to download stations: ${res.statusText}`);
|
|
87
86
|
data = await res.json();
|
|
88
|
-
await
|
|
87
|
+
await cache.set('stations', data);
|
|
89
88
|
}
|
|
90
89
|
return new this(data.stations);
|
|
91
90
|
}
|
|
@@ -17,18 +17,14 @@ function default_1(app) {
|
|
|
17
17
|
},
|
|
18
18
|
start({ stormglassApiKey } = {}) {
|
|
19
19
|
app.debug("Using StormGlass.io API");
|
|
20
|
-
return async (
|
|
21
|
-
const { date = (0, moment_1.default)().subtract(1, "days").toISOString() } = params;
|
|
20
|
+
return async ({ position, date = (0, moment_1.default)().subtract(1, "days").toISOString() }) => {
|
|
22
21
|
const endPoint = new URL("https://api.stormglass.io/v2/tide/extremes/point");
|
|
23
|
-
const position = app.getSelfPath("navigation.position.value");
|
|
24
|
-
if (!position)
|
|
25
|
-
throw new Error("no position");
|
|
26
22
|
endPoint.search = new URLSearchParams({
|
|
27
23
|
start: (0, moment_1.default)(date).format("YYYY-MM-DD"),
|
|
28
24
|
end: (0, moment_1.default)(date).add(7, "days").format("YYYY-MM-DD"),
|
|
29
25
|
// datum: "CD",
|
|
30
|
-
lat: position.latitude,
|
|
31
|
-
lng: position.longitude
|
|
26
|
+
lat: position.latitude.toString(),
|
|
27
|
+
lng: position.longitude.toString()
|
|
32
28
|
}).toString();
|
|
33
29
|
app.debug("Fetching StormGlass.io: " + endPoint.toString());
|
|
34
30
|
const res = await fetch(endPoint, {
|
|
@@ -40,7 +36,7 @@ function default_1(app) {
|
|
|
40
36
|
app.debug(JSON.stringify(data, null, 2));
|
|
41
37
|
return {
|
|
42
38
|
station: {
|
|
43
|
-
name: data.meta.station.name
|
|
39
|
+
name: `${data.meta.station.name} (${data.meta.station.source})`,
|
|
44
40
|
position: {
|
|
45
41
|
latitude: data.meta.station.lat,
|
|
46
42
|
longitude: data.meta.station.lng,
|
|
@@ -17,20 +17,17 @@ function default_1(app) {
|
|
|
17
17
|
},
|
|
18
18
|
start({ worldtidesApiKey = '' } = {}) {
|
|
19
19
|
app.debug("Using WorldTides API");
|
|
20
|
-
return async (params
|
|
21
|
-
const { date = (0, moment_1.default)().subtract(1, "days") } = params;
|
|
20
|
+
return async (params) => {
|
|
21
|
+
const { position, date = (0, moment_1.default)().subtract(1, "days") } = params;
|
|
22
22
|
const endPoint = new URL("https://www.worldtides.info/api/v3");
|
|
23
|
-
const position = app.getSelfPath("navigation.position.value");
|
|
24
|
-
if (!position)
|
|
25
|
-
throw new Error("no position");
|
|
26
23
|
endPoint.search = new URLSearchParams({
|
|
27
24
|
date: (0, moment_1.default)(date).format("YYYY-MM-DD"),
|
|
28
25
|
datum: "CD",
|
|
29
26
|
days: "7",
|
|
30
27
|
extremes: "true",
|
|
31
28
|
key: worldtidesApiKey,
|
|
32
|
-
lat: position.latitude,
|
|
33
|
-
lon: position.longitude,
|
|
29
|
+
lat: position.latitude.toString(),
|
|
30
|
+
lon: position.longitude.toString(),
|
|
34
31
|
}).toString();
|
|
35
32
|
app.debug("Fetching worldtides: " + endPoint.toString());
|
|
36
33
|
const res = await fetch(endPoint);
|
|
@@ -42,7 +39,7 @@ function default_1(app) {
|
|
|
42
39
|
throw new Error("worldtides data: " + data.error ? data.error : "none");
|
|
43
40
|
return {
|
|
44
41
|
station: {
|
|
45
|
-
name:
|
|
42
|
+
name: `${data.station} (${data.atlas})`,
|
|
46
43
|
position: {
|
|
47
44
|
latitude: data.responseLat,
|
|
48
45
|
longitude: data.responseLon,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-tides",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Tidal predictions for the vessel's position from various online sources.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -38,24 +38,22 @@
|
|
|
38
38
|
"@eslint/js": "^9.21.0",
|
|
39
39
|
"@tailwindcss/vite": "^4.1.3",
|
|
40
40
|
"@types/d3": "^7.4.3",
|
|
41
|
-
"@types/node": "^
|
|
41
|
+
"@types/node": "^24.3.1",
|
|
42
42
|
"@types/react": "^19.0.12",
|
|
43
43
|
"@types/react-dom": "^19.0.4",
|
|
44
|
-
"@
|
|
45
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
44
|
+
"@vitejs/plugin-react": "^5.0.2",
|
|
46
45
|
"d3": "^7.9.0",
|
|
47
46
|
"eslint": "^9.21.0",
|
|
48
47
|
"eslint-plugin-react-hooks": "^5.1.0",
|
|
49
48
|
"eslint-plugin-react-refresh": "^0.4.19",
|
|
50
|
-
"globals": "^
|
|
49
|
+
"globals": "^16.3.0",
|
|
51
50
|
"react": "^19.1.0",
|
|
52
51
|
"react-dom": "^19.1.0",
|
|
53
|
-
"recharts": "^2.15.1",
|
|
54
52
|
"signalk-server": "^2.13.5",
|
|
55
53
|
"tailwindcss": "^4.1.3",
|
|
56
54
|
"typescript": "^5.8.2",
|
|
57
55
|
"typescript-eslint": "^8.24.1",
|
|
58
|
-
"vite": "^
|
|
56
|
+
"vite": "^7.1.5"
|
|
59
57
|
},
|
|
60
58
|
"repository": {
|
|
61
59
|
"type": "git",
|