signalk-tides 1.5.0 → 2.0.0-beta.1

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.
@@ -20,8 +20,8 @@ jobs:
20
20
  - name: Run linter
21
21
  run: npm run lint
22
22
 
23
- build:
24
- name: Build
23
+ test:
24
+ name: Test
25
25
  runs-on: ubuntu-latest
26
26
  env:
27
27
  CI: true
@@ -35,5 +35,31 @@ jobs:
35
35
  - name: Install dependencies
36
36
  run: npm install
37
37
 
38
+ - name: Install Playwright browsers
39
+ run: npx playwright install chromium
40
+
38
41
  - name: Run build
39
42
  run: npm run build
43
+
44
+ - name: Run Tests
45
+ run: npm run test
46
+
47
+ publish:
48
+ needs: [lint, test]
49
+ runs-on: ubuntu-latest
50
+ permissions:
51
+ id-token: write
52
+ pull-requests: write
53
+ steps:
54
+ - uses: actions/checkout@v6
55
+
56
+ - uses: actions/setup-node@v6
57
+
58
+ - name: Install modules
59
+ run: npm install
60
+
61
+ - name: Build
62
+ run: npm run build
63
+
64
+ - name: Publish to pkg.pr.new
65
+ run: npx pkg-pr-new publish
@@ -0,0 +1 @@
1
+ [ 2058ms] [ERROR] Failed to load resource: net::ERR_NETWORK_IO_SUSPENDED @ https://docs.stormglass.io/_media/logo.svg:0
@@ -0,0 +1 @@
1
+ [ 239ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk-tides/assets/icon.svg:0
@@ -0,0 +1,44 @@
1
+ [ 260ms] [LOG] [ConnectionStateMachine] Service initialized @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
2
+ [ 265ms] [LOG] [AppInit Network Service] Connection Configuration not found. Creating configuration using Auto-Discovery URL: http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
3
+ [ 268ms] [LOG] [ConnectionStateMachine] Starting HTTP discovery: Connecting to http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
4
+ [ 268ms] [LOG] [Connection Service] Connecting to: http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
5
+ [ 279ms] [LOG] [ConnectionStateMachine] HTTP discovery successful @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
6
+ [ 279ms] [LOG] [AppSettings Service] Service startup... @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:3
7
+ [ 279ms] [LOG] [AppSettings Service] LocalStorage enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
8
+ [ 279ms] [LOG] [AppSettings Service] Error loading appConfig config. Force loading appConfig defaults @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
9
+ [ 279ms] [LOG] [AppSettings Service] Error loading dashboardsConfig config. Force loading dashboardsConfig defaults @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
10
+ [ 279ms] [LOG] [AppSettings Service] Error loading themeConfig config. Force loading themeConfig defaults @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
11
+ [ 280ms] [LOG] [AppSettings Service] Saving Application config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
12
+ [ 280ms] [LOG] [DatasetStreamService] Auto Starting 0 Datasets @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:0
13
+ [ 280ms] [LOG] [AppInit Network Service] Initialization completed @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
14
+ [ 280ms] [LOG] [ConnectionStateMachine] WebSocket mode enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
15
+ [ 280ms] [LOG] [AppInit Network Service] Starting WebSocket connection after initialization @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
16
+ [ 280ms] [LOG] [ConnectionStateMachine] Starting WebSocket connection @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
17
+ [ 280ms] [LOG] [Delta Service] ConnectionStateMachine request: WebSocket opening... @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
18
+ [ 287ms] [WARNING] [Dashboard Service] No dashboards found in settings, creating blank dashboard @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:0
19
+ [ 289ms] [LOG] [Remote Dashboards] Cleaning paths on server @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
20
+ [ 290ms] [LOG] [UI Event Service] PWA mode: false @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:49
21
+ [ 300ms] [LOG] *********** KIP Version Information *********** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
22
+ [ 300ms] [LOG] ** App Version: 4.8.0 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
23
+ [ 300ms] [LOG] ** Browser Version: Chrome 149 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
24
+ [ 300ms] [LOG] ** OS Version: macOS @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
25
+ [ 300ms] [LOG] *********************************************** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
26
+ [ 305ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
27
+ [ 323ms] [LOG] [Delta Service] WebSocket connected without Authorization Token @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
28
+ [ 324ms] [LOG] [ConnectionStateMachine] WebSocket connected @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
29
+ [ 324ms] [LOG] [Connection Service] Server Name: signalk-server, Version: 2.23.0-beta.1, Roles: ["master","main"] @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
30
+ [ 437ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
31
+ [ 437ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
32
+ [ 437ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
33
+ [ 450ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3
34
+ [ 515ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@signalk/freeboard-sk/:0
35
+ [ 1311ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/freeboard-sk:0
36
+ [ 1387ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip:0
37
+ [ 1387ms] [LOG] [KipSeriesApiClientService] POST http://localhost:3000/plugins/kip/series/reconcile (0 series) @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
38
+ [ 1403ms] [INFO] Internet connection detected. @ http://localhost:3000/@signalk/freeboard-sk/main-YHMZXR5G.js:249
39
+ [ 1414ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip/series/reconcile:0
40
+ [ 1414ms] [ERROR] [KipSeriesApiClientService] Series reconcile request failed: Ar @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
41
+ [ 1517ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk/v1/api/vessels/self/navigation/anchor:0
42
+ [ 1784ms] [WARNING] Blocked aria-hidden on an element because its descendant retained focus. The focus must not be hidden from assistive technology users. Avoid using aria-hidden on a focused element or its ancestor. Consider using the inert attribute instead, which will also prevent focus. For more details, see the aria-hidden section of the WAI-ARIA specification at https://w3c.github.io/aria/#aria-hidden.
43
+ Element with focus: <div>
44
+ Ancestor with aria-hidden: <app-root> @ http://localhost:3000/@signalk/freeboard-sk/:0
@@ -0,0 +1 @@
1
+ [ 68ms] [LOG] [Delta Service] App suspended @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
@@ -0,0 +1,60 @@
1
+ [ 188ms] [LOG] [ConnectionStateMachine] Service initialized @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
2
+ [ 191ms] [LOG] [ConnectionStateMachine] Starting HTTP discovery: Connecting to http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
3
+ [ 192ms] [LOG] [Connection Service] Connecting to: http://localhost:3000 @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
4
+ [ 213ms] [LOG] [ConnectionStateMachine] HTTP discovery successful @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
5
+ [ 214ms] [LOG] [AppSettings Service] Service startup... @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:3
6
+ [ 214ms] [LOG] [AppSettings Service] LocalStorage enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
7
+ [ 214ms] [LOG] [DatasetStreamService] Auto Starting 0 Datasets @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:0
8
+ [ 214ms] [LOG] [AppInit Network Service] Initialization completed @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
9
+ [ 214ms] [LOG] [ConnectionStateMachine] WebSocket mode enabled @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
10
+ [ 214ms] [LOG] [AppInit Network Service] Starting WebSocket connection after initialization @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
11
+ [ 214ms] [LOG] [ConnectionStateMachine] Starting WebSocket connection @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
12
+ [ 215ms] [LOG] [Delta Service] ConnectionStateMachine request: WebSocket opening... @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
13
+ [ 220ms] [LOG] [Remote Dashboards] Cleaning paths on server @ http://localhost:3000/@mxtommy/kip/main-UBENKC2D.js:0
14
+ [ 221ms] [LOG] [UI Event Service] PWA mode: false @ http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:49
15
+ [ 226ms] [LOG] *********** KIP Version Information *********** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
16
+ [ 226ms] [LOG] ** App Version: 4.8.0 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
17
+ [ 226ms] [LOG] ** Browser Version: Chrome 149 @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
18
+ [ 226ms] [LOG] ** OS Version: macOS @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
19
+ [ 226ms] [LOG] *********************************************** @ http://localhost:3000/@mxtommy/kip/chunk-NS2OV2OW.js:0
20
+ [ 229ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
21
+ [ 248ms] [LOG] [Delta Service] WebSocket connected without Authorization Token @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
22
+ [ 248ms] [LOG] [ConnectionStateMachine] WebSocket connected @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
23
+ [ 249ms] [LOG] [Connection Service] Server Name: signalk-server, Version: 2.23.0-beta.1, Roles: ["master","main"] @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:0
24
+ [ 370ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
25
+ [ 370ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
26
+ [ 370ms] [WARNING] [Request Service] Status Code: 403 - DENIED: Authorization with R/W or Admin permission level is required to send commands. Configure Sign In credential. @ http://localhost:3000/@mxtommy/kip/chunk-W6MCE3GH.js:0
27
+ [ 380ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3
28
+ [ 476ms] [WARNING] An iframe which has both allow-scripts and allow-same-origin for its sandbox attribute can escape its sandboxing. @ http://localhost:3000/@signalk/freeboard-sk/:0
29
+ [ 934ms] [INFO] Internet connection detected. @ http://localhost:3000/@signalk/freeboard-sk/main-YHMZXR5G.js:249
30
+ [ 940ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/freeboard-sk:0
31
+ [ 1000ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip:0
32
+ [ 1000ms] [LOG] [KipSeriesApiClientService] POST http://localhost:3000/plugins/kip/series/reconcile (0 series) @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
33
+ [ 1007ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/kip/series/reconcile:0
34
+ [ 1007ms] [ERROR] [KipSeriesApiClientService] Series reconcile request failed: Ar @ http://localhost:3000/@mxtommy/kip/chunk-6ZR3M3IQ.js:2
35
+ [ 1073ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk/v1/api/vessels/self/navigation/anchor:0
36
+ [ 87530ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/freeboard-sk:0
37
+ [ 87531ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/anchoralarm:0
38
+ [ 87532ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/tracks:0
39
+ [ 87532ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/signalk-polar-performance-plugin:0
40
+ [ 87532ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/resources-provider:0
41
+ [ 87536ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/signalk-racer:0
42
+ [ 87572ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/course-provider:0
43
+ [ 87573ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/autopilot:0
44
+ [ 87573ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/plugins/pypilot-autopilot-provider:0
45
+ [ 112153ms] [ERROR] ERROR _: NG0951
46
+ at Object.computation (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:94168)
47
+ at Object.producerRecomputeValue (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3:33213)
48
+ at Qn (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3:30918)
49
+ at n.r [as iframe] (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3:32825)
50
+ at n.ngAfterViewInit (http://localhost:3000/@mxtommy/kip/chunk-5SMHTIVS.js:44:361583)
51
+ at ng (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:27312)
52
+ at rI (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:27474)
53
+ at Gg (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:27201)
54
+ at wa (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:26929)
55
+ at bS (http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:4:67813) @ http://localhost:3000/@mxtommy/kip/chunk-LEY6MANN.js:3
56
+ [ 263184ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
57
+ [ 263185ms] [LOG] [AppSettings Service] Saving Application config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
58
+ [ 399636ms] [LOG] [AppSettings Service] Saving Dashboard config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
59
+ [ 399636ms] [LOG] [AppSettings Service] Saving Application config to LocalStorage @ http://localhost:3000/@mxtommy/kip/chunk-2TP7C66X.js:4
60
+ [ 4767580ms] [LOG] [Delta Service] App suspended @ http://localhost:3000/@mxtommy/kip/chunk-IXUKD73N.js:0
@@ -0,0 +1,5 @@
1
+ [ 2353ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/skServer/plugins:0
2
+ [ 2353ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/skServer/appstore/available:0
3
+ [ 2353ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ http://localhost:3000/skServer/security/access/requests:0
4
+ [ 2427ms] [LOG] connected @ http://localhost:3000/admin/assets/bootstrap-BbNbkfZh.js:69
5
+ [ 3712ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ http://localhost:3000/signalk-usage/assets/icons/signalk-usage-icon_72x72.png:0
package/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # signalk-tides
2
2
 
3
- A SignalK plugin that provides tidal predictions for the vessel's position from various [sources](#sources).
3
+ A SignalK plugin that provides offline tidal predictions for the vessel's position, powered by [Neaps](https://github.com/neaps/neaps).
4
+
5
+ Since 2.0, predictions are computed locally from harmonic constituents — no network access or API keys required.
4
6
 
5
7
  ## Installation
6
8
 
7
- 1. Install `signalk-tides` from the SignalK Appstore or manually by running `npm install signalk-tides` in the SignalK server directory (`~/.signalk`).
9
+ Install `signalk-tides` from the SignalK Appstore or manually by running `npm install signalk-tides` in the SignalK server directory (`~/.signalk`).
8
10
 
9
- 2. Optionally go to the plugin settings in "Server => Plugin Config => Tides" and configure which source to use.
10
11
 
11
12
  ## Usage
12
13
 
@@ -20,205 +21,44 @@ It publishes the following [tide data](https://signalk.org/specification/1.7.0/d
20
21
  * `environment.tide.timeLow`
21
22
  * `environment.tide.heightNow`
22
23
  * `environment.tide.stationName`
24
+ * `environment.tide.state` — tide trend, `rising` or `falling`
25
+ * `environment.tide.timeToNextExtreme` — seconds until the next high or low water
23
26
 
24
- ### Tides resource
27
+ ### Tides API
25
28
 
26
- It also registers a `tides` resource, which returns tide predictions for yesterday and the next 6 days for the vessel's position.
29
+ The plugin mounts the [Neaps API](https://github.com/neaps/neaps) at `/signalk/v2/api/tides`, which serves station search, extremes, and timeline predictions. The synthetic station `vessel/current` resolves to the nearest station to the vessel:
27
30
 
28
31
  ```
29
- $ curl http://localhost:3000/signalk/v2/api/resources/tides
32
+ $ curl http://localhost:3000/signalk/v2/api/tides/stations/vessel/current/extremes
30
33
  ```
31
34
 
32
- ##### Request Parameters
35
+ ### Tides resource
33
36
 
34
- * `date` (optional) - the date for which to get the tide predictions in the format `YYYY-MM-DD`. Default: yesterday's date.
37
+ It also registers a `tides` resource, which returns the next 7 days of tide extremes for the vessel's position.
38
+
39
+ ```
40
+ $ curl http://localhost:3000/signalk/v2/api/resources/tides
41
+ ```
35
42
 
36
43
  ##### Response
37
44
 
38
45
  ```json
39
46
  {
40
- "extremes" : [
41
- {
42
- "time" : "2025-03-29T00:45:00.000Z",
43
- "type" : "Low",
44
- "value" : 0.025
45
- },
46
- {
47
- "time" : "2025-03-29T07:20:00.000Z",
48
- "type" : "High",
49
- "value" : 1.928
50
- },
51
- {
52
- "time" : "2025-03-29T13:18:00.000Z",
53
- "type" : "Low",
54
- "value" : 0.044
55
- },
56
- {
57
- "time" : "2025-03-29T19:45:00.000Z",
58
- "type" : "High",
59
- "value" : 1.815
60
- },
61
- {
62
- "time" : "2025-03-30T01:24:00.000Z",
63
- "type" : "Low",
64
- "value" : 0.164
65
- },
66
- {
67
- "time" : "2025-03-30T07:52:00.000Z",
68
- "type" : "High",
69
- "value" : 2.024
70
- },
71
- {
72
- "time" : "2025-03-30T14:05:00.000Z",
73
- "type" : "Low",
74
- "value" : -0.131
75
- },
76
- {
77
- "time" : "2025-03-30T20:43:00.000Z",
78
- "type" : "High",
79
- "value" : 1.733
80
- },
81
- {
82
- "time" : "2025-03-31T02:05:00.000Z",
83
- "type" : "Low",
84
- "value" : 0.334
85
- },
86
- {
87
- "time" : "2025-03-31T08:27:00.000Z",
88
- "type" : "High",
89
- "value" : 2.084
90
- },
91
- {
92
- "time" : "2025-03-31T14:53:00.000Z",
93
- "type" : "Low",
94
- "value" : -0.247
95
- },
96
- {
97
- "time" : "2025-03-31T21:44:00.000Z",
98
- "type" : "High",
99
- "value" : 1.632
100
- },
101
- {
102
- "time" : "2025-04-01T02:48:00.000Z",
103
- "type" : "Low",
104
- "value" : 0.517
105
- },
106
- {
107
- "time" : "2025-04-01T09:06:00.000Z",
108
- "type" : "High",
109
- "value" : 2.094
110
- },
111
- {
112
- "time" : "2025-04-01T15:46:00.000Z",
113
- "type" : "Low",
114
- "value" : -0.29
115
- },
116
- {
117
- "time" : "2025-04-01T22:51:00.000Z",
118
- "type" : "High",
119
- "value" : 1.529
120
- },
121
- {
122
- "time" : "2025-04-02T03:37:00.000Z",
123
- "type" : "Low",
124
- "value" : 0.695
125
- },
126
- {
127
- "time" : "2025-04-02T09:49:00.000Z",
128
- "type" : "High",
129
- "value" : 2.05
130
- },
131
- {
132
- "time" : "2025-04-02T16:43:00.000Z",
133
- "type" : "Low",
134
- "value" : -0.263
135
- },
136
- {
137
- "time" : "2025-04-03T00:04:00.000Z",
138
- "type" : "High",
139
- "value" : 1.448
140
- },
141
- {
142
- "time" : "2025-04-03T04:35:00.000Z",
143
- "type" : "Low",
144
- "value" : 0.847
145
- },
146
- {
147
- "time" : "2025-04-03T10:39:00.000Z",
148
- "type" : "High",
149
- "value" : 1.957
150
- },
151
- {
152
- "time" : "2025-04-03T17:50:00.000Z",
153
- "type" : "Low",
154
- "value" : -0.189
155
- },
156
- {
157
- "time" : "2025-04-04T01:24:00.000Z",
158
- "type" : "High",
159
- "value" : 1.421
160
- },
161
- {
162
- "time" : "2025-04-04T05:49:00.000Z",
163
- "type" : "Low",
164
- "value" : 0.946
165
- },
166
- {
167
- "time" : "2025-04-04T11:40:00.000Z",
168
- "type" : "High",
169
- "value" : 1.837
170
- },
171
- {
172
- "time" : "2025-04-04T19:07:00.000Z",
173
- "type" : "Low",
174
- "value" : -0.11
175
- },
176
- {
177
- "time" : "2025-04-05T02:40:00.000Z",
178
- "type" : "High",
179
- "value" : 1.455
180
- },
181
- {
182
- "time" : "2025-04-05T07:19:00.000Z",
183
- "type" : "Low",
184
- "value" : 0.958
185
- },
186
- {
187
- "time" : "2025-04-05T12:53:00.000Z",
188
- "type" : "High",
189
- "value" : 1.728
190
- },
191
- {
192
- "time" : "2025-04-05T20:25:00.000Z",
193
- "type" : "Low",
194
- "value" : -0.061
195
- }
196
- ],
197
- "station" : {
198
- "name" : "Rincon Point, Pier 22 1/2",
199
- "position" : {
200
- "latitude" : 37.79,
201
- "longitude" : -122.387
202
- }
203
- }
47
+ "datum": "MLLW",
48
+ "units": "meters",
49
+ "station": {
50
+ "id": "noaa/9414290",
51
+ "name": "San Francisco",
52
+ "latitude": 37.806,
53
+ "longitude": -122.465
54
+ },
55
+ "extremes": [
56
+ { "time": "2025-03-29T00:45:00.000Z", "level": 0.025, "high": false, "low": true, "label": "Low" },
57
+ { "time": "2025-03-29T07:20:00.000Z", "level": 1.928, "high": true, "low": false, "label": "High" }
58
+ ]
204
59
  }
205
60
  ```
206
61
 
207
- ## Sources
208
-
209
- - [Neaps](https://github.com/neaps/neaps)
210
- - Offline, open source, runs locally
211
- - Coverage: Regional — United States and territories (currently)
212
- - [NOAA](https://tidesandcurrents.noaa.gov/web_services_info.html)
213
- - Online (NOAA web services)
214
- - Regional — United States and territories
215
- - [WorldTides API](https://www.worldtides.info/)
216
- - Online (requires API key, subject to API quotas)
217
- - Coverage: Global - station and model coverage varies by location
218
- - [StormGlass.io](https://stormglass.io/)
219
- - Online (requires API key)
220
- - Coverage: Global - aggregated marine data; tide availability varies
221
-
222
62
  ## License
223
63
 
224
64
  This plugin is a fork of the [signalk-tides-api](https://github.com/joabakk/signalk-tides-api) plugin (which is no longer working) and is licensed under the [Apache License 2.0](LICENSE). Kudos to @joabakk and @sbender9 for the original work.
@@ -1,24 +1,37 @@
1
- /** Given a list of tide extremes, estimate the height at a specific time. */
2
- export function approximateTideHeightAt(extremes, time) {
3
- const sorted = extremes.slice().sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime());
4
- const prev = sorted.filter(h => new Date(h.time) <= time).at(-1);
5
- const next = sorted.filter(h => new Date(h.time) >= time).at(0);
6
- if (!prev)
7
- throw new Error("Missing height data before " + time.toISOString());
1
+ /**
2
+ * Trend of the tide at `time`: "rising" if the next extreme is a High,
3
+ * "falling" if it is a Low.
4
+ *
5
+ * Boundary: at the instant of an extreme the extreme itself satisfies
6
+ * `extreme.time >= time`, so it is still "next", and the state reads as that
7
+ * extreme's own type — e.g. "falling" with timeToNextExtreme 0 at the instant
8
+ * of low water — flipping on the following tick. Simplest consistent rule at
9
+ * one-minute resolution.
10
+ *
11
+ * Returns null when the forecast has no upcoming extreme (stale data).
12
+ */
13
+ export function tideStateAt(extremes, time) {
14
+ const next = nextExtremeAt(extremes, time);
8
15
  if (!next)
9
- throw new Error("Missing height data after " + time.toISOString());
10
- const progress = (time.getTime() - new Date(prev.time).getTime()) /
11
- (new Date(next.time).getTime() - new Date(prev.time).getTime());
12
- const value = interpolate(prev.value, next.value, easeSine(progress));
13
- return parseFloat(value.toFixed(3));
16
+ return null;
17
+ return next.high ? "rising" : "falling";
14
18
  }
15
- /** Interpolate between two values using the given progress (0-1). */
16
- function interpolate(start, end, progress) {
17
- return start + (end - start) * progress;
19
+ /** First extreme at or after `time`, or undefined. Tolerates unsorted input. */
20
+ function nextExtremeAt(extremes, time) {
21
+ return extremes
22
+ .slice()
23
+ .sort((a, b) => a.time.getTime() - b.time.getTime())
24
+ .find((extreme) => extreme.time >= time);
18
25
  }
19
- function easeSine(progress) {
20
- // Map progress [0..1] to angle [0..π]
21
- const angle = progress * Math.PI;
22
- // Use sine to ease in/out
23
- return (1 - Math.cos(angle)) / 2;
26
+ /**
27
+ * Whole seconds (rounded up) until the next tide extreme (high or low water)
28
+ * at `time`, or null when the forecast has no upcoming extreme. Ceiling keeps
29
+ * the countdown > 0 until the extreme is actually reached; see tideStateAt
30
+ * for the exactly-at-extreme boundary behaviour (this returns 0 for that tick).
31
+ */
32
+ export function timeToNextExtreme(extremes, time) {
33
+ const next = nextExtremeAt(extremes, time);
34
+ if (!next)
35
+ return null;
36
+ return Math.ceil((next.time.getTime() - time.getTime()) / 1000);
24
37
  }